From 9d578c2d86e951c870f16bbba5b4a8668e6dbcc1 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 6 Aug 2023 12:14:02 +0100 Subject: [PATCH] i3-cli-download-dir (#12) Closes kemitix/podal#3 Reviewed-on: https://git.kemitix.net/kemitix/podal/pulls/12 Co-authored-by: Paul Campbell Co-committed-by: Paul Campbell --- Cargo.lock | 20 ++++++++++++++++++++ Cargo.toml | 2 +- src/errors.rs | 13 ++++++++++++- src/file/env.rs | 17 ++++++++++++----- src/file/read.rs | 27 +++++++++++++++------------ src/history/add.rs | 29 ++++++++++++++++++----------- src/history/find.rs | 34 +++++++++++++++++++++++++--------- src/lib.rs | 27 +++++++++++++++++++++------ src/main.rs | 6 +++++- src/params/mod.rs | 10 ++++++++++ src/test_utils.rs | 16 +++++++++------- 11 files changed, 148 insertions(+), 53 deletions(-) create mode 100644 src/params/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 04cc534..0dd5107 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" dependencies = [ "clap_builder", + "clap_derive", + "once_cell", ] [[package]] @@ -197,6 +199,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.27", +] + [[package]] name = "clap_lex" version = "0.5.0" @@ -545,6 +559,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 3c17ea9..04a0466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" atom_syndication = "0.12.1" reqwest = { version = "0.11.18", features = ["json", "blocking"] } scraper = "0.17.1" -clap = "4.3.19" +clap = {version = "4.3.19", features = ["derive"]} bytes = "1.4.0" [dev-dependencies] diff --git a/src/errors.rs b/src/errors.rs index d8f4236..1135c29 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -3,11 +3,13 @@ use std::{str::Utf8Error, string::FromUtf8Error}; #[derive(Debug)] pub struct Error { pub details: String, + pub source: String, } impl Error { pub fn message(details: &str) -> Self { Self { details: details.to_string(), + source: "(not provided)".to_string(), } } } @@ -15,6 +17,7 @@ impl From for Error { fn from(value: Utf8Error) -> Self { Self { details: value.to_string(), + source: "Utf8Error".to_string(), } } } @@ -22,18 +25,23 @@ impl From for Error { fn from(value: FromUtf8Error) -> Self { Self { details: value.to_string(), + source: "FromUtf8Error".to_string(), } } } impl From for Error { fn from(details: String) -> Self { - Self { details } + Self { + details, + source: "String".to_string(), + } } } impl From for Error { fn from(value: std::io::Error) -> Self { Self { details: value.to_string(), + source: "std::io::Error".to_string(), } } } @@ -41,6 +49,7 @@ impl From> for Error { fn from(value: std::sync::mpsc::SendError) -> Self { Self { details: value.to_string(), + source: "std::sync::mpsc::SendError".to_string(), } } } @@ -48,6 +57,7 @@ impl From for Error { fn from(value: atom_syndication::Error) -> Self { Self { details: value.to_string(), + source: "atom_syndication::Error".to_string(), } } } @@ -55,6 +65,7 @@ impl From for Error { fn from(value: reqwest::Error) -> Self { Self { details: value.to_string(), + source: "reqwest::Error".to_string(), } } } diff --git a/src/file/env.rs b/src/file/env.rs index 2cdc30d..1718b75 100644 --- a/src/file/env.rs +++ b/src/file/env.rs @@ -11,16 +11,23 @@ pub struct FileEnv { pub open: FileOpenFn, pub append_line: FileAppendLineFn, } -impl Default for FileEnv { - fn default() -> Self { +impl FileEnv { + pub fn create(directory: String) -> Self { + let open_dir = directory.clone(); + let append_dir = directory.clone(); Self { - open: Box::new(|path| Ok(File::open(path)?)), - append_line: Box::new(|file_name, line| { + open: Box::new(move |file_name| { + let path = format!("{}/{}", &open_dir, file_name); + let file = File::open(&path)?; + Ok(file) + }), + append_line: Box::new(move |file_name, line| { + let path = format!("{}/{}", &append_dir, file_name); let mut file = OpenOptions::new() .write(true) .append(true) .create(true) - .open(file_name) + .open(&path) .unwrap(); writeln!(file, "{}", line)?; Ok(()) diff --git a/src/file/read.rs b/src/file/read.rs index 2bb892f..a5b6926 100644 --- a/src/file/read.rs +++ b/src/file/read.rs @@ -25,14 +25,15 @@ mod tests { #[test] fn can_load_file() -> Result<()> { //given - let (dir, file_name) = create_text_file( - "subscriptions.txt", + let file_name = "subscriptions.txt"; + let dir = create_text_file( + file_name, include_bytes!("../../test/data/subscriptions.txt"), )?; - let file_env = FileEnv::default(); + let file_env = FileEnv::create(dir.path().to_string_lossy().to_string()); //when - let result = lines_from(&file_name, &file_env)?; + let result = lines_from(file_name, &file_env)?; //then drop(dir); @@ -43,14 +44,15 @@ mod tests { #[test] fn ignores_blank_lines() -> Result<()> { //given - let (dir, file_name) = create_text_file( - "subscriptions.txt", + let file_name = "subscriptions.txt"; + let dir = create_text_file( + file_name, include_bytes!("../../test/data/subscriptions-blank-line.txt"), )?; - let file_env = FileEnv::default(); + let file_env = FileEnv::create(dir.path().to_string_lossy().to_string()); //when - let result = lines_from(&file_name, &file_env)?; + let result = lines_from(file_name, &file_env)?; //then drop(dir); @@ -61,14 +63,15 @@ mod tests { #[test] fn ignores_comments() -> Result<()> { //given - let (dir, file_name) = create_text_file( - "subscriptions.txt", + let file_name = "subscriptions.txt"; + let dir = create_text_file( + file_name, include_bytes!("../../test/data/subscriptions-comment.txt"), )?; - let file_env = FileEnv::default(); + let file_env = FileEnv::create(dir.path().to_string_lossy().to_string()); //when - let result = lines_from(&file_name, &file_env)?; + let result = lines_from(file_name, &file_env)?; //then drop(dir); diff --git a/src/history/add.rs b/src/history/add.rs index 90e2628..492916b 100644 --- a/src/history/add.rs +++ b/src/history/add.rs @@ -21,9 +21,10 @@ mod tests { #[test] fn creates_file_if_missing() -> Result<()> { //given - let (dir, file_name) = - create_text_file("download.txt", include_bytes!("../../test/data/empty.txt"))?; - std::fs::remove_file(&file_name)?; + let file_name = "download.txt"; + let dir = create_text_file(file_name, include_bytes!("../../test/data/empty.txt"))?; + let path = format!("{}/{}", dir.path().to_string_lossy(), file_name); + std::fs::remove_file(path)?; let link = Link { href: "foo".to_string(), @@ -34,10 +35,14 @@ mod tests { length: None, }; //when - add(&link, &file_name, &FileEnv::default())?; + add( + &link, + file_name, + &FileEnv::create(dir.path().to_string_lossy().to_string()), + )?; //then - let content: Vec = read_text_file(&file_name)?; + let content: Vec = read_text_file(dir.path(), file_name)?; drop(dir); let expected = vec!["foo".to_string()]; @@ -49,10 +54,8 @@ mod tests { #[test] fn appends_to_exising_file() -> Result<()> { // given - let (dir, file_name) = create_text_file( - "download.txt", - include_bytes!("../../test/data/downloads.txt"), - )?; + let file_name = "download.txt"; + let dir = create_text_file(file_name, include_bytes!("../../test/data/downloads.txt"))?; let link = Link { href: "foo".to_string(), @@ -63,10 +66,14 @@ mod tests { length: None, }; //when - add(&link, &file_name, &FileEnv::default())?; + add( + &link, + file_name, + &FileEnv::create(dir.path().to_string_lossy().to_string()), + )?; //then - let content: Vec = read_text_file(&file_name)?; + let content: Vec = read_text_file(dir.path(), file_name)?; drop(dir); let expected = vec![ diff --git a/src/history/find.rs b/src/history/find.rs index 720b62b..63c236b 100644 --- a/src/history/find.rs +++ b/src/history/find.rs @@ -24,8 +24,8 @@ mod test { #[test] fn true_if_line_exists() -> Result<()> { //given - let (dir, file_name) = - create_text_file("file", include_bytes!("../../test/data/with-llamma.txt"))?; + let file_name = "file"; + let dir = create_text_file(file_name, include_bytes!("../../test/data/with-llamma.txt"))?; let link = Link { href: "llamma".to_string(), rel: "".to_string(), @@ -35,7 +35,11 @@ mod test { length: None, }; //when - let result = find(&link, &file_name, &FileEnv::default())?; + let result = find( + &link, + file_name, + &FileEnv::create(dir.path().to_string_lossy().to_string()), + )?; //then drop(dir); @@ -48,8 +52,11 @@ mod test { #[test] fn false_if_line_absent() -> Result<()> { //given - let (dir, file_name) = - create_text_file("file", include_bytes!("../../test/data/without-llamma.txt"))?; + let file_name = "file"; + let dir = create_text_file( + file_name, + include_bytes!("../../test/data/without-llamma.txt"), + )?; let link = Link { href: "llamma".to_string(), rel: "".to_string(), @@ -60,7 +67,11 @@ mod test { }; //when - let result = find(&link, &file_name, &FileEnv::default())?; + let result = find( + &link, + file_name, + &FileEnv::create(dir.path().to_string_lossy().to_string()), + )?; //then drop(dir); @@ -73,8 +84,9 @@ mod test { #[test] fn false_if_embedded_within_line() -> Result<()> { //given - let (dir, file_name) = create_text_file( - "file", + let file_name = "file"; + let dir = create_text_file( + file_name, include_bytes!("../../test/data/with-embedded-llamma.txt"), )?; let link = Link { @@ -87,7 +99,11 @@ mod test { }; //when - let result = find(&link, &file_name, &FileEnv::default())?; + let result = find( + &link, + file_name, + &FileEnv::create(dir.path().to_string_lossy().to_string()), + )?; //then drop(dir); diff --git a/src/lib.rs b/src/lib.rs index d59f443..67ceced 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod feed; pub mod file; pub mod history; pub mod network; +pub mod params; pub mod prelude; #[cfg(test)] @@ -57,11 +58,12 @@ mod tests { let (tx, rx) = mpsc::channel::(); // channel to recieve notice of downloaded urls // two channels in subscriptions.txt - let (subs_dir, subs_file_name) = - create_text_file("subs", "@channel1\nignore me\n@channel2".as_bytes())?; + let subs_file_name = "subs"; + let subs_dir = + create_text_file(subs_file_name, "@channel1\nignore me\n@channel2".as_bytes())?; // one item from each channel is already listed in the downloads.txt file - let (history_dir, history_file_name) = - create_text_file("history", "c1-f2\nc2-f3".as_bytes())?; + let history_file_name = "history"; + let history_dir = create_text_file(history_file_name, "c1-f2\nc2-f3".as_bytes())?; let env = Env { network: NetworkEnv { @@ -82,13 +84,26 @@ mod tests { download_as_mp3: mock_network_download_as_mp3(tx), }, file: FileEnv { - open: mock_file_open(vec![subs_file_name.clone(), history_file_name.clone()]), + open: mock_file_open(HashMap::from([ + ( + subs_file_name.to_string(), + format!("{}/{}", subs_dir.path().to_string_lossy(), subs_file_name), + ), + ( + history_file_name.to_string(), + format!( + "{}/{}", + history_dir.path().to_string_lossy(), + history_file_name + ), + ), + ])), append_line: mock_file_append_line(), }, }; //when - run(&subs_file_name, &history_file_name, site, env)?; + run(subs_file_name, history_file_name, site, env)?; //then drop(subs_dir); drop(history_dir); diff --git a/src/main.rs b/src/main.rs index c1c31ca..8a0034e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ +use clap::Parser; use podal::file::FileEnv; use podal::network::NetworkEnv; +use podal::params::Args; use podal::prelude::*; fn main() -> Result<()> { @@ -8,13 +10,15 @@ fn main() -> Result<()> { let history = "downloaded.txt"; let site = "https://www.youtube.com/"; + let args = Args::parse(); + podal::run( subscriptions, history, site, podal::Env { network: NetworkEnv::default(), - file: FileEnv::default(), + file: FileEnv::create(args.directory), }, )?; diff --git a/src/params/mod.rs b/src/params/mod.rs new file mode 100644 index 0000000..f92355f --- /dev/null +++ b/src/params/mod.rs @@ -0,0 +1,10 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +pub struct Args { + /// The directory to download mp3 files into. + /// This is also the directory where the subscription and history files are stored. + /// Defaults to the current directory + #[arg(short, long, default_value = ".")] + pub directory: String, +} diff --git a/src/test_utils.rs b/src/test_utils.rs index 5afc9d5..a29fcfa 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, fs::{read_to_string, File}, io::Write, + path::Path, str::from_utf8, sync::mpsc::Sender, }; @@ -15,16 +16,17 @@ use crate::{ prelude::*, }; -pub fn create_text_file(name: &str, data: &[u8]) -> Result<(TempDir, String)> { +pub fn create_text_file(name: &str, data: &[u8]) -> Result { let data = from_utf8(data)?; let dir = tempdir()?; let filename = format!("{}", &dir.path().join(name).display()); - let file = File::create(&filename)?; + let file = File::create(filename)?; write!(&file, "{data}")?; - Ok((dir, filename)) + Ok(dir) } -pub fn read_text_file(file_name: &str) -> Result> { +pub fn read_text_file(path: &Path, file_name: &str) -> Result> { + let file_name = format!("{}/{}", path.to_str().unwrap(), file_name); Ok(read_to_string(file_name)? .lines() .map(String::from) @@ -58,10 +60,10 @@ pub fn mock_network_fetch_as_bytes_with_rss_entries( } }) } -pub fn mock_file_open(real_paths: Vec) -> FileOpenFn { +pub fn mock_file_open(real_paths: HashMap) -> FileOpenFn { Box::new(move |path: &str| { - if real_paths.contains(&path.to_string()) { - Ok(File::open(path)?) + if let Some(real_path) = real_paths.get(&path.to_string()) { + Ok(File::open(real_path)?) } else { Err(Error::message( format!("Not implemented: file_open: {}", path).as_str(),