diff options
-rw-r--r-- | Cargo.lock | 93 | ||||
-rw-r--r-- | modderbaas/Cargo.toml | 1 | ||||
-rw-r--r-- | modderbaas/src/download.rs | 141 | ||||
-rw-r--r-- | modderbaas/src/error.rs | 13 |
4 files changed, 232 insertions, 16 deletions
@@ -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")] |