aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock93
-rw-r--r--modderbaas/Cargo.toml1
-rw-r--r--modderbaas/src/download.rs141
-rw-r--r--modderbaas/src/error.rs13
4 files changed, 232 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 86e8fd5..363c6d4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -99,6 +99,9 @@ name = "cc"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
+dependencies = [
+ "jobserver",
+]
[[package]]
name = "cfg-if"
@@ -321,6 +324,21 @@ dependencies = [
]
[[package]]
+name = "git2"
+version = "0.13.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a8057932925d3a9d9e4434ea016570d37420ddb1ceed45a174d577f24ed6700"
+dependencies = [
+ "bitflags",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
+[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -388,6 +406,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
+name = "jobserver"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
+dependencies = [
+ "libc",
+]
+
+[[package]]
name = "js-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -409,6 +436,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
[[package]]
+name = "libgit2-sys"
+version = "0.12.24+1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddbd6021eef06fb289a8f54b3c2acfdd85ff2a585dfbb24b8576325373d2152c"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "libssh2-sys"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "lock_api"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -482,6 +549,7 @@ name = "modderbaas"
version = "0.1.0"
dependencies = [
"dirs",
+ "git2",
"indoc",
"itertools",
"log",
@@ -563,6 +631,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1256,6 +1343,12 @@ dependencies = [
]
[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/modderbaas/Cargo.toml b/modderbaas/Cargo.toml
index 2c2918c..1945b3e 100644
--- a/modderbaas/Cargo.toml
+++ b/modderbaas/Cargo.toml
@@ -8,6 +8,7 @@ edition = "2018"
[dependencies]
dirs = "4.0.0"
+git2 = "0.13.23"
itertools = "0.10.1"
log = "0.4.14"
once_cell = "1.8.0"
diff --git a/modderbaas/src/download.rs b/modderbaas/src/download.rs
index b9507b7..dfe7522 100644
--- a/modderbaas/src/download.rs
+++ b/modderbaas/src/download.rs
@@ -9,6 +9,7 @@
//! the API to get the right download URL.
//! * [`Source::ModId`]: Refers to a simple mod name. Note that this specification can be
//! ambiguous, in which case the [`Downloader`] will return an error.
+//! * [`Source::Git`]: Refers to a git repository.
//!
//! The actual download work is done by a [`Downloader`]. Each [`Downloader`] has its own temporary
//! directory, in which any mods are downloaded and extracted. If you drop the [`Downloader`],
@@ -18,9 +19,12 @@
use std::{
fs,
io::{Cursor, Read},
+ path::Path,
str::FromStr,
};
+use git2::build::RepoBuilder;
+use log::debug;
use tempdir::TempDir;
use url::Url;
use uuid::Uuid;
@@ -43,6 +47,11 @@ pub enum Source {
///
/// The download may fail if there are multiple mods providing the same ID.
ModId(ModId),
+ /// Clone a mod repository through `git`.
+ ///
+ /// The URL should point to the repository, a specific branch can be selected by specifying a
+ /// fragment (such as `#development`).
+ Git(Url),
}
impl FromStr for Source {
@@ -53,6 +62,11 @@ impl FromStr for Source {
let url = Url::parse(s)?;
return Ok(Source::Http(url));
}
+
+ if let Some(remainder) = s.strip_prefix("git+") {
+ return Ok(Source::Git(Url::parse(remainder)?));
+ }
+
let groups = regex!("^([^/]+)/([^/]+)$").captures(s);
if let Some(groups) = groups {
return Ok(Source::ContentDb((
@@ -117,6 +131,7 @@ impl Downloader {
}
self.download_http(&candidates[0].url)
}
+ Source::Git(ref url) => self.clone_git(url),
}
}
@@ -139,23 +154,123 @@ impl Downloader {
fs::create_dir(&dir)?;
archive.extract(&dir)?;
+ open_dir_or_contained(&dir).map_err(|_| Error::NoModInArchive(url.clone()))
+ }
- // Some archives contain the mod files directly, so try to open it:
- if let Ok(pack) = minemod::open_mod_or_pack(&dir) {
- return Ok(pack);
+ /// Download a mod by cloning the Git repository.
+ ///
+ /// See [`Source::Git`] for more information about the construction of the URL.
+ pub fn clone_git(&self, url: &Url) -> Result<Box<dyn ModContainer>> {
+ let repo_url = {
+ let mut copy = url.clone();
+ copy.set_fragment(None);
+ copy
+ };
+
+ let dir = self
+ .temp_dir
+ .path()
+ .join(&Uuid::new_v4().to_hyphenated().to_string());
+
+ let mut builder = RepoBuilder::new();
+
+ if let Some(branch) = url.fragment() {
+ builder.branch(branch);
}
- // If the archive does not contain the mod directly, we instead try the subdirectories that
- // we've extracted.
- for entry in fs::read_dir(&dir)? {
- let entry = entry?;
- let metadata = fs::metadata(&entry.path())?;
- if metadata.is_dir() {
- if let Ok(pack) = minemod::open_mod_or_pack(&entry.path()) {
- return Ok(pack);
- }
+ debug!("Cloning {} to {:?}", url, dir);
+ let repository = builder.clone(repo_url.as_ref(), &dir)?;
+
+ // We're not really interested in many git operations, and would probably prefer to not
+ // copy the git directory to the mod install dir (which might or might not work anyway,
+ // depending on whether the mod resides in the repository root).
+ //
+ // Therefore, we simply delete the .git folder :)
+ fs::remove_dir_all(repository.path())?;
+
+ open_dir_or_contained(&dir).map_err(|_| Error::NoModInRepository(url.clone()))
+ }
+}
+
+fn open_dir_or_contained(dir: &Path) -> Result<Box<dyn ModContainer>> {
+ // Some archives contain the mod files directly, so try to open it:
+ if let Ok(pack) = minemod::open_mod_or_pack(&dir) {
+ return Ok(pack);
+ }
+
+ // If the archive does not contain the mod directly, we instead try the subdirectories that
+ // we've extracted.
+ for entry in fs::read_dir(&dir)? {
+ let entry = entry?;
+ let metadata = fs::metadata(&entry.path())?;
+ if metadata.is_dir() {
+ if let Ok(pack) = minemod::open_mod_or_pack(&entry.path()) {
+ return Ok(pack);
}
}
- Err(Error::InvalidModDir(dir))
+ }
+ Err(Error::InvalidModDir(dir.into()))
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ fn url(str: &str) -> Url {
+ Url::parse(str).unwrap()
+ }
+
+ #[test]
+ fn test_source_from_http() {
+ let cases = &[
+ ("http://localhost", Source::Http(url("http://localhost"))),
+ ("https://localhost", Source::Http(url("https://localhost"))),
+ (
+ "https://example.com:123/path?do=download",
+ Source::Http(url("https://example.com:123/path?do=download")),
+ ),
+ ];
+ for (input, expected) in cases {
+ assert_eq!(&input.parse::<Source>().unwrap(), expected);
+ }
+ }
+
+ #[test]
+ fn test_source_from_content_id() {
+ let cases = &[
+ ("foo/bar", Source::ContentDb(("foo".into(), "bar".into()))),
+ (
+ "TenPlus1/mobs",
+ Source::ContentDb(("TenPlus1".into(), "mobs".into())),
+ ),
+ ];
+ for (input, expected) in cases {
+ assert_eq!(&input.parse::<Source>().unwrap(), expected);
+ }
+ }
+
+ #[test]
+ fn test_source_from_mod_id() {
+ let cases = &[("mobs", Source::ModId("mobs".into()))];
+ for (input, expected) in cases {
+ assert_eq!(&input.parse::<Source>().unwrap(), expected);
+ }
+ }
+
+ #[test]
+ fn test_source_from_git() {
+ let cases = &[
+ (
+ "git+https://localhost",
+ Source::Git(url("https://localhost")),
+ ),
+ (
+ "git+https://example.com/repo.git#devel",
+ Source::Git(url("https://example.com/repo.git#devel")),
+ ),
+ ];
+ for (input, expected) in cases {
+ assert_eq!(&input.parse::<Source>().unwrap(), expected);
+ }
}
}
diff --git a/modderbaas/src/error.rs b/modderbaas/src/error.rs
index df78c2f..15064f2 100644
--- a/modderbaas/src/error.rs
+++ b/modderbaas/src/error.rs
@@ -6,6 +6,7 @@
use std::path::PathBuf;
use thiserror::Error;
+use url::Url;
/// The main error type.
#[derive(Error, Debug)]
@@ -32,9 +33,12 @@ pub enum Error {
#[error("'{0}' does not represent a valid mod source")]
InvalidSourceSpec(String),
- /// An empty ZIP archive was downloaded.
- #[error("the downloaded file was empty")]
- EmptyArchive,
+ /// No mod found in the downloaded archive.
+ #[error("the downloaded archive from '{0}' did not contain a mod")]
+ NoModInArchive(Url),
+ /// No mod found in the repository.
+ #[error("the repository at '{0}' did not contain a mod")]
+ NoModInRepository(Url),
/// The given world does not have a game ID set.
#[error("the world has no game ID set")]
NoGameSet,
@@ -60,6 +64,9 @@ pub enum Error {
/// Wrapper for URL parsing errors.
#[error("invalid URL")]
UrlError(#[from] url::ParseError),
+ /// Wrapper for Git errors.
+ #[error("git error")]
+ GitError(#[from] git2::Error),
/// Wrapper for Unix errors.
#[cfg(unix)]
#[error("underlying Unix error")]