diff --git a/Cargo.toml b/Cargo.toml index 6b07273..20e98cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ clap = { version = "4.3.8", features = ["cargo", "derive", "env", "wrap_help"] } directories = "5.0.1" downloader = { version = "0.2.7", default-features = false, features = ["rustls-tls"] } indoc = "2.0.1" +rand = "0.8.5" regex = "1.8.4" serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.99" diff --git a/src/main.rs b/src/main.rs index c0bbd38..e8c59f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ -use std::{path::PathBuf, sync::OnceLock}; +use std::{collections::HashMap, fs, path::PathBuf, sync::OnceLock}; -use anyhow::{ensure, Context}; +use anyhow::Context; use clap::{Parser, Subcommand}; use config::Config; use directories::{ProjectDirs, UserDirs}; @@ -29,10 +29,7 @@ fn main() -> anyhow::Result<()> { .set(user_dirs) .ok() .context("failed to initialize user directories")?; - PROJ_DIRS - .set(proj_dirs) - .ok() - .context("failed to initialize program directories")?; + let proj_dirs = PROJ_DIRS.get_or_init(|| proj_dirs); if let CMD::License = cli.command { println!("{}", include_str!("../LICENSE")); @@ -41,7 +38,7 @@ fn main() -> anyhow::Result<()> { // prepare for operation let cfg = Config::read_from_default_file().context("failed to load config")?; - let downloader = cfg.downloader().context("failed to create downloader")?; + let mut downloader = cfg.downloader().context("failed to create downloader")?; let mut persistent = PersistentState::read_from_default_file().context("failed to load persistent state")?; @@ -52,6 +49,71 @@ fn main() -> anyhow::Result<()> { CMD::PersistentState => { println!("{persistent}"); } + CMD::Download { name } => { + let mut cache = proj_dirs.cache_dir().to_path_buf(); + cache.push(&format!("{:0>16x}", rand::random::())); + fs::create_dir_all(&cache).context("failed to create cache dir")?; + + let name = name + .as_ref() + .map(|n| n.as_str()) + .or(persistent.list()) + .context("no list specified or selected")?; + let list = TargetList::load(&name).context("failed to load list")?; + let mut downloads = list.downloads(); + + let mut mapping = HashMap::with_capacity(downloads.len()); + let mut counter = 0; + for d in &mut downloads { + let mut cache_path = cache.clone(); + cache_path.push(format!("{counter:0>16x}")); + let prev = std::mem::replace(&mut d.file_name, cache_path.clone()); + mapping.insert(cache_path, prev); + counter += 1; + } + + let results = downloader + .download(&downloads) + .context("all downloads failed")?; + + for res in results { + if res.is_err() { + eprintln!("{:?}", res.context("download_failed").unwrap_err()); + continue; + } + + let res = res.unwrap(); + let res = mapping + .remove(&res.file_name) + .context("target file name missing") + .map(|target| { + if target.is_absolute() { + target + } else { + let mut path = cfg.base_directory.clone(); + path.push(target); + path + } + }) + .and_then(|target| { + fs::rename(&res.file_name, target).context("failed to move cached result") + }); + + if let Err(err) = res { + eprintln!("{:?}", err); + } + } + + for (leftover, _) in mapping { + if let Err(err) = + fs::remove_file(leftover).context("failed to delete leftover cache file") + { + eprintln!("{err:?}"); + } + } + + fs::remove_dir(cache).context("failed to delete cache directory")?; + } CMD::List { cmd } => match cmd { ListCommand::Create { name, @@ -132,6 +194,11 @@ enum CMD { License, /// Print the current persistent state. PersistentState, + /// Download a target list. + Download { + /// The name of the target list. Defaults to the selected list. + name: Option, + }, /// Target list operations. List { #[clap(subcommand)] diff --git a/src/persistent_state.rs b/src/persistent_state.rs index cfc8098..d87b322 100644 --- a/src/persistent_state.rs +++ b/src/persistent_state.rs @@ -9,7 +9,7 @@ use anyhow::{bail, ensure, Context}; use indoc::writedoc; use serde::{Deserialize, Serialize}; -use crate::{target_list::TargetList, PROJ_DIRS}; +use crate::PROJ_DIRS; const PERSISTENT_FILE: &str = "persistent_state.json"; diff --git a/src/target.rs b/src/target.rs index 860e897..a48f936 100644 --- a/src/target.rs +++ b/src/target.rs @@ -50,7 +50,7 @@ impl Display for Target { } } -impl Into for Target { +impl Into for &Target { fn into(self) -> Download { match self.urls.len() { 0 => panic!("target without url"), diff --git a/src/target_list.rs b/src/target_list.rs index 50b29e3..332558c 100644 --- a/src/target_list.rs +++ b/src/target_list.rs @@ -4,7 +4,8 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::{ensure, Context, Result}; +use anyhow::{ensure, Context}; +use downloader::Download; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -28,6 +29,10 @@ impl TargetList { pub fn len_targets(&self) -> usize { self.inner.targets.len() } + + pub fn downloads(&self) -> Vec { + self.inner.targets.iter().map(|t| t.into()).collect() + } } impl TargetList { @@ -54,8 +59,6 @@ impl TargetList { } pub fn new(name: &str, comment: Option<&str>) -> anyhow::Result { - let dirs = PROJ_DIRS.get().expect("directories not initialized"); - let dir = Self::list_dir(); ensure!( dir.is_dir() || !dir.exists(),