diff --git a/Cargo.lock b/Cargo.lock index 193afd2..38eb66a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -681,6 +681,21 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "git2" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf97ba92db08df386e10c8ede66a2a0369bd277090afd8710e19e38de9ec0cd" +dependencies = [ + "bitflags 2.4.0", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "h2" version = "0.3.21" @@ -923,6 +938,46 @@ version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +[[package]] +name = "libgit2-sys" +version = "0.16.1+1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.10" @@ -1002,6 +1057,7 @@ dependencies = [ "confy", "error-chain", "futures-util", + "git2", "indicatif", "openssl", "rand", @@ -1014,6 +1070,7 @@ dependencies = [ "tokio", "trauma", "url", + "walkdir", "zip", ] @@ -1601,6 +1658,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.22" @@ -2179,6 +2245,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2314,6 +2390,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index d7f0cb5..47a2f19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ regex = "1.10.2" async-recursion = "1.0.5" openssl = { version = "0.10.45", features = ["vendored"] } url = "2.2" +git2 = "0.18.1" +walkdir = "2.4.0" [dependencies.confy] features = ["toml_conf"] diff --git a/src/cmd/install.rs b/src/cmd/install.rs index 1118d1c..f72ebb1 100644 --- a/src/cmd/install.rs +++ b/src/cmd/install.rs @@ -1,24 +1,37 @@ extern crate serde; extern crate serde_ini; -use crate::utils::{confirm, download_from_url, get_last, get_mmrl_json, is_url}; use crate::android_root::{get_downloads_dir, get_install_cli}; use crate::repo::{find_module, find_version, get_id_details, Module}; -use reqwest::Client; +use crate::utils::{confirm, download_from_url, get_last, get_mmrl_json, is_git, is_url, zip_dir}; use async_recursion::async_recursion; +use git2::Repository; +use reqwest::Client; +use std::fs::{self, File}; use std::io::{BufRead, BufReader, Error, ErrorKind}; use std::path::Path; use std::process::{exit, Command, Stdio}; +use walkdir::WalkDir; #[async_recursion] async fn check_requires( + _is_git: bool, path: String, install_requires: bool, client: Client, yes: bool, modules: &Vec, -) { - let mini = get_mmrl_json(&path); +) -> () { + let mini: Result; + + if _is_git { + mini = match File::open(path) { + Ok(file) => serde_json::from_reader(file), + Err(..) => serde_json::from_str("{\"require\":[]}"), + }; + } else { + mini = get_mmrl_json(&path); + } for req in mini.unwrap().require { let dep_path = Path::new("/data/adb/modules") @@ -30,7 +43,7 @@ async fn check_requires( if !(dep_path_update.exists() || dep_path.exists()) { if install_requires { println!("Install requires"); - install(client.clone(), yes, install_requires, modules, req).await; + install(client.clone(), yes, install_requires, modules, req).await; } else { println!("This module requires {} to be installed", req.clone()); exit(1) @@ -39,31 +52,82 @@ async fn check_requires( } } +const METHOD_STORED: Option = Some(zip::CompressionMethod::Stored); + #[async_recursion] pub async fn install(client: Client, yes: bool, requires: bool, modules: &Vec, id: String) { let _url = &id.to_owned()[..]; - if !is_url(_url) { - let (_id, _ver) = get_id_details(id); - let module = find_module(&modules, _id.clone()); - let version = find_version(module.versions.clone(), _ver); + if is_git(_url) { + let name = get_last(_url); + let path = &[get_downloads_dir(), name.unwrap()].join("/"); + match Repository::clone(_url, path) { + Ok(repo) => repo, + Err(e) => panic!("failed to clone: {}", e), + }; + + check_requires( + true, + [path, "mmrl.json"].join("/"), + requires, + client.clone(), + yes, + modules, + ) + .await; + + let file = File::create([path, "zip"].join(".")).unwrap(); + + let walkdir = WalkDir::new(path); + let it = walkdir.into_iter(); + + zip_dir( + &mut it.filter_map(|e| e.ok()), + path, + file, + METHOD_STORED.unwrap(), + ) + .unwrap(); + + if Path::new(path).exists() { + fs::remove_dir_all(path).expect("File delete failed"); + } + + println!("Info not availabe in git install"); + let success = yes || confirm("Do you want to continue [y/N] "); + + if success { + let (bin, args) = get_install_cli(&path); + + let stdout = Command::new(bin) + .args(args) + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .stdout + .ok_or_else(|| Error::new(ErrorKind::Other, "Could not capture standard output.")) + .unwrap(); + + let reader = BufReader::new(stdout); + reader + .lines() + .filter_map(|line| line.ok()) + .for_each(|line| println!("{}", line)); + } else { + exit(0); + } + } else if is_url(_url) { + let name = get_last(_url); let path = &[ get_downloads_dir(), - [ - [version.version.clone(), module.id].join("-"), - "zip".to_string(), - ] - .join("."), + [name.clone().unwrap().to_string(), "zip".to_string()].join("."), ] .join("/"); + download_from_url(client.clone(), id.clone(), name.unwrap(), path).await; + check_requires(false, path.clone(), requires, client.clone(), yes, modules).await; - println!("Downloading {}", module.name); - println!("Version: {}", &version.version); - - download_from_url(client.clone(), version.zip_url, module.name, path).await; - check_requires(path.clone(), requires, client.clone(), yes, modules).await; - - let success = yes || confirm("Do you want to continue [y/N]? "); + println!("Info not availabe in url install"); + let success = yes || confirm("Do you want to continue [y/N] "); if success { let (bin, args) = get_install_cli(&path); @@ -87,17 +151,27 @@ pub async fn install(client: Client, yes: bool, requires: bool, modules: &Vec Result { Ok(file) => file, Err(..) => { println!("mmrl.json not found"); - return serde_json::from_str("{\"require\":[]}") + return serde_json::from_str("{\"require\":[]}"); } }; @@ -115,3 +124,49 @@ pub fn is_url(url: &str) -> bool { let url_regex: &str = r"https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}(\.[a-z]{2,4})?\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)"; return Regex::new(url_regex).unwrap().is_match(url); } + +pub fn is_git(url: &str) -> bool { + let git_regex: &str = + r"((git|ssh|http(s)?)|(git@[\w\.]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)(\/)?"; + return Regex::new(git_regex).unwrap().is_match(url); +} + +pub fn zip_dir( + it: &mut dyn Iterator, + prefix: &str, + writer: T, + method: zip::CompressionMethod, +) -> zip::result::ZipResult<()> +where + T: Write + Seek, +{ + let mut zip = zip::ZipWriter::new(writer); + let options = FileOptions::default() + .compression_method(method) + .unix_permissions(0o755); + + let mut buffer = Vec::new(); + for entry in it { + let path = entry.path(); + let name = path.strip_prefix(Path::new(prefix)).unwrap(); + + // Write file or directory explicitly + // Some unzip tools unzip files with directory paths correctly, some do not! + if path.is_file() { + println!("adding file {:?} as {:?} ...", path, name); + zip.start_file_from_path(name, options)?; + let mut f = File::open(path)?; + + f.read_to_end(&mut buffer)?; + zip.write_all(&*buffer)?; + buffer.clear(); + } else if name.as_os_str().len() != 0 { + // Only if not root! Avoids path spec / warning + // and mapname conversion failed error on unzip + println!("adding dir {:?} as {:?} ...", path, name); + zip.add_directory_from_path(name, options)?; + } + } + zip.finish()?; + Result::Ok(()) +}