diff --git a/README.md b/README.md index 8f1279e..7965603 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ Note: this project is still in early-development, so expect breaking changes. # gday +[![Crates.io Version](https://img.shields.io/crates/v/gday_server)](https://crates.io/crates/gday_server) A command line tool for sending files. diff --git a/gday/README.md b/gday/README.md index 8ce3b88..f54a6f9 100644 --- a/gday/README.md +++ b/gday/README.md @@ -1,6 +1,7 @@ Note: this project is still in early-development, so expect breaking changes. # gday +[![Crates.io Version](https://img.shields.io/crates/v/gday_server)](https://crates.io/crates/gday_server) A command line tool for sending files. diff --git a/gday_file_transfer/src/file_meta.rs b/gday_file_transfer/src/file_meta.rs index 918d423..18ff0c1 100644 --- a/gday_file_transfer/src/file_meta.rs +++ b/gday_file_transfer/src/file_meta.rs @@ -7,7 +7,7 @@ use std::{ }; /// Information about an offered file. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct FileMeta { /// The path offered to the peer pub short_path: PathBuf, @@ -16,7 +16,7 @@ pub struct FileMeta { } /// Information about a locally stored file -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct FileMetaLocal { /// The shortened path that will be offered to the peer pub short_path: PathBuf, @@ -73,6 +73,8 @@ impl FileMeta { if number == 0 { Ok(None) + } else if number == 1 { + Ok(Some(path)) } else { suffix_with_number(&mut path, number - 1); Ok(Some(path)) @@ -210,12 +212,14 @@ pub fn get_file_metas(paths: &[PathBuf]) -> Result, Error> { let b = &paths[j]; // we don't want two top-level folders or files with the same name - // then we'd run into weird cases with FileMetaLocal.short_path + // then we'd run into ambiguity with FileMetaLocal.short_path if a.file_name() == b.file_name() && a.is_file() == b.is_file() { let name = a.file_name().unwrap_or(OsStr::new("")).to_os_string(); return Err(Error::PathsHaveSameName(name)); } + // we don't want one path to be a prefix of another, or we'd + // get duplicates if a.starts_with(b) { return Err(Error::PathIsPrefix(b.to_path_buf(), a.to_path_buf())); } @@ -263,13 +267,13 @@ fn get_file_metas_helper( .to_path_buf(); // get the file's size - let size = path.metadata()?.len(); + let len = path.metadata()?.len(); // insert this file metadata into set let meta = FileMetaLocal { local_path: path.to_path_buf(), short_path, - len: size, + len, }; files.push(meta); } diff --git a/gday_file_transfer/src/lib.rs b/gday_file_transfer/src/lib.rs index da7a7bf..d95988f 100644 --- a/gday_file_transfer/src/lib.rs +++ b/gday_file_transfer/src/lib.rs @@ -110,7 +110,11 @@ pub enum Error { )] PathIsPrefix(PathBuf, PathBuf), - /// Two folders or files have same name. This would make the offered metadata ambiguous. - #[error("Two folders or files have same name: '{0:?}'. This would make the offered metadata ambiguous.")] + /// Two of the given folders or files have same name. + /// This would make the offered metadata ambiguous. + #[error( + "Two of the given folders or files have same name: '{0:?}'. + This would make the offered metadata ambiguous." + )] PathsHaveSameName(std::ffi::OsString), } diff --git a/gday_file_transfer/src/offer.rs b/gday_file_transfer/src/offer.rs index 04bdeed..4ab94e2 100644 --- a/gday_file_transfer/src/offer.rs +++ b/gday_file_transfer/src/offer.rs @@ -99,8 +99,8 @@ impl FileResponseMsg { ) -> Result { let mut response = Vec::with_capacity(offer.files.len()); - for offered in &offer.files { - if offered.already_exists(save_dir)? { + for file_meta in &offer.files { + if file_meta.already_exists(save_dir)? { // reject response.push(None); } else { @@ -168,7 +168,7 @@ impl FileResponseMsg { /// Returns the number of non-rejected files. pub fn get_num_not_rejected(&self) -> usize { - self.response.iter().filter_map(|f| *f).count() + self.response.iter().filter(|f| f.is_some()).count() } /// Returns the total number of only partially accepted files. diff --git a/gday_file_transfer/tests/integration_test.rs b/gday_file_transfer/tests/integration_test.rs index 1e32100..aed4e9d 100644 --- a/gday_file_transfer/tests/integration_test.rs +++ b/gday_file_transfer/tests/integration_test.rs @@ -1,12 +1,12 @@ #![forbid(unsafe_code)] #![warn(clippy::all)] use gday_file_transfer::{ - get_file_metas, read_from, receive_files, send_files, write_to, FileMeta, FileMetaLocal, - FileOfferMsg, FileResponseMsg, + get_file_metas, read_from, receive_files, send_files, write_to, FileMetaLocal, FileOfferMsg, + FileResponseMsg, }; use std::fs::{self, create_dir_all}; use std::io::Write; -use std::net::{Ipv6Addr, SocketAddr}; +use std::net::SocketAddr; use std::{fs::File, path::PathBuf}; /// Returns a temporary directory @@ -94,9 +94,9 @@ fn test_get_file_metas_1() { let test_dir = make_test_dir(); let dir_path = test_dir.path(); let dir_name = PathBuf::from(dir_path.file_name().unwrap()); - let files = gday_file_transfer::get_file_metas(&[dir_path.to_path_buf()]).unwrap(); + let mut result = gday_file_transfer::get_file_metas(&[dir_path.to_path_buf()]).unwrap(); - let expected = [ + let mut expected = [ FileMetaLocal { short_path: dir_name.join("file1"), local_path: dir_path.join("file1"), @@ -147,10 +147,10 @@ fn test_get_file_metas_1() { }, ]; - assert_eq!(files.len(), expected.len()); - for e in expected { - assert!(files.contains(&e)); - } + result.sort_unstable(); + expected.sort_unstable(); + + assert_eq!(result, expected); } /// Confirm that [`get_file_metas()`] returns @@ -160,14 +160,14 @@ fn test_get_file_metas_2() { let test_dir = make_test_dir(); let dir_path = test_dir.path(); - let files = gday_file_transfer::get_file_metas(&[ + let mut result = gday_file_transfer::get_file_metas(&[ dir_path.join("dir/subdir1/"), dir_path.join("dir/subdir2/file1"), dir_path.join("dir/subdir2/file2.txt"), ]) .unwrap(); - let expected = [ + let mut expected = [ FileMetaLocal { short_path: PathBuf::from("subdir1/file1"), local_path: dir_path.join("dir/subdir1/file1"), @@ -198,17 +198,17 @@ fn test_get_file_metas_2() { }, ]; - assert_eq!(files.len(), expected.len()); - for e in expected { - assert!(files.contains(&e)); - } + result.sort_unstable(); + expected.sort_unstable(); + + assert_eq!(result, expected); } /// Test the file transfer. #[test] fn file_transfer() { // The loopback address that peer_a will connect to. - let pipe_addr = SocketAddr::from((Ipv6Addr::LOCALHOST, 2000)); + let pipe_addr: SocketAddr = "[::1]:2000".parse().unwrap(); // Listens on the loopback address let listener = std::net::TcpListener::bind(pipe_addr).unwrap(); @@ -230,9 +230,9 @@ fn file_transfer() { ]; let file_metas = get_file_metas(&paths).unwrap(); let file_offer = FileOfferMsg::from(file_metas.clone()); - write_to(file_offer, &mut stream_a).unwrap(); - // read the response from the peer + // send offer, and read response + write_to(file_offer, &mut stream_a).unwrap(); let response: FileResponseMsg = read_from(&mut stream_a).unwrap(); // send the files @@ -314,56 +314,3 @@ fn file_transfer() { assert!(fs::read(dir_b.path().join("dir/subdir2/file1")).is_err()); assert!(fs::read(dir_b.path().join("dir/subdir2/file2.txt")).is_err()); } - -/// Test serializing and deserializing [`FileOfferMsg`] and [`FileResponseMsg`]. -#[test] -fn sending_messages() { - let mut pipe = std::collections::VecDeque::new(); - - for msg in get_offer_msg_examples() { - write_to(msg, &mut pipe).unwrap(); - } - - for msg in get_offer_msg_examples() { - let deserialized_msg: FileOfferMsg = read_from(&mut pipe).unwrap(); - assert_eq!(msg, deserialized_msg); - } - - for msg in get_response_msg_examples() { - write_to(msg, &mut pipe).unwrap(); - } - - for msg in get_response_msg_examples() { - let deserialized_msg: FileResponseMsg = read_from(&mut pipe).unwrap(); - assert_eq!(msg, deserialized_msg); - } -} - -fn get_offer_msg_examples() -> Vec { - vec![ - FileOfferMsg { - files: vec![ - FileMeta { - short_path: PathBuf::from("example/path"), - len: 43, - }, - FileMeta { - short_path: PathBuf::from("/foo/hello"), - len: 50, - }, - ], - }, - FileOfferMsg { files: Vec::new() }, - ] -} - -fn get_response_msg_examples() -> Vec { - vec![ - FileResponseMsg { - response: vec![None, Some(0), Some(100)], - }, - FileResponseMsg { - response: vec![None, None, None], - }, - ] -} diff --git a/gday_file_transfer/tests/test_file_meta.rs b/gday_file_transfer/tests/test_file_meta.rs new file mode 100644 index 0000000..09bfbda --- /dev/null +++ b/gday_file_transfer/tests/test_file_meta.rs @@ -0,0 +1,140 @@ +use gday_file_transfer::FileMeta; +use std::io::Write; +use std::{fs::File, path::PathBuf}; + +/// Tests methods of [`FileMeta`] with a non-empty directory. +#[test] +fn test_file_meta_1() { + // create test directory + let temp_dir = tempfile::tempdir().unwrap(); + let dir_path = temp_dir.path(); + std::fs::create_dir_all(dir_path.join("fol der")).unwrap(); + + let mut f = File::create_new(dir_path.join("fol der/file.tar.gz")).unwrap(); + write!(f, "---").unwrap(); + + let mut f = File::create_new(dir_path.join("fol der/file (1).tar.gz")).unwrap(); + write!(f, "---").unwrap(); + + let mut f = File::create_new(dir_path.join("fol der/file.tar.gz.part5")).unwrap(); + write!(f, "--").unwrap(); + + let file_meta = FileMeta { + short_path: PathBuf::from("fol der/file.tar.gz"), + len: 5, + }; + + // save path is the save directory joined with the short path + let save_path = file_meta.get_save_path(dir_path); + assert_eq!(save_path, dir_path.join("fol der/file.tar.gz")); + + // unoccupied path should increment the appended number by one + let save_path = file_meta.get_unoccupied_save_path(dir_path).unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file (2).tar.gz")); + + // last occupied path + let save_path = file_meta + .get_last_occupied_save_path(dir_path) + .unwrap() + .unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file (1).tar.gz")); + + // the file exists, but has the wrong size + let already_exists = file_meta.already_exists(dir_path).unwrap(); + assert!(!already_exists); + + // the path should be suffixed with "part" and the length of the file + let save_path = file_meta.get_partial_download_path(dir_path).unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file.tar.gz.part5")); + + // a partial download does exist + let partial_exists = file_meta.partial_download_exists(dir_path).unwrap(); + assert_eq!(partial_exists, Some(2)); +} + +/// Tests methods of [`FileMeta`] with a non-empty directory. +#[test] +fn test_file_meta_2() { + // create test directory + let temp_dir = tempfile::tempdir().unwrap(); + let dir_path = temp_dir.path(); + std::fs::create_dir_all(dir_path.join("fol der")).unwrap(); + + let mut f = File::create_new(dir_path.join("fol der/file.tar.gz")).unwrap(); + write!(f, "---").unwrap(); + + let mut f = File::create_new(dir_path.join("fol der/file (1).tar.gz")).unwrap(); + write!(f, "-----").unwrap(); + + let mut f = File::create_new(dir_path.join("fol der/file.tar.gz.part7")).unwrap(); + write!(f, "--").unwrap(); + + let file_meta = FileMeta { + short_path: PathBuf::from("fol der/file.tar.gz"), + len: 5, + }; + + // save path is the save directory joined with the short path + let save_path = file_meta.get_save_path(dir_path); + assert_eq!(save_path, dir_path.join("fol der/file.tar.gz")); + + // unoccupied path should increment the appended number by one + let save_path = file_meta.get_unoccupied_save_path(dir_path).unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file (2).tar.gz")); + + // last occupied path + let save_path = file_meta + .get_last_occupied_save_path(dir_path) + .unwrap() + .unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file (1).tar.gz")); + + // the file exists with the right size + let already_exists = file_meta.already_exists(dir_path).unwrap(); + assert!(already_exists); + + // the path should be suffixed with "part" and the length of the file + let save_path = file_meta.get_partial_download_path(dir_path).unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file.tar.gz.part5")); + + // the partial download file has the wrong size suffix + let partial_exists = file_meta.partial_download_exists(dir_path).unwrap(); + assert_eq!(partial_exists, None); +} + +/// Tests methods of [`FileMeta`] with an empty directory. +#[test] +fn test_file_meta_empty() { + // create test directory that is empty + let temp_dir = tempfile::tempdir().unwrap(); + let dir_path = temp_dir.path(); + + let file_meta = FileMeta { + short_path: PathBuf::from("fol der/file.tar.gz"), + len: 5, + }; + + // save path is the save directory joined with the short path + let save_path = file_meta.get_save_path(dir_path); + assert_eq!(save_path, dir_path.join("fol der/file.tar.gz")); + + // unoccupied path should increment the appended number by one + let save_path = file_meta.get_unoccupied_save_path(dir_path).unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file.tar.gz")); + + // last occupied path + let save_path = file_meta.get_last_occupied_save_path(dir_path).unwrap(); + assert!(save_path.is_none()); + + // the file doesn't exist yet + let already_exists = file_meta.already_exists(dir_path).unwrap(); + assert!(!already_exists); + + // the path should be suffixed with "part" and the length of the file + let save_path = file_meta.get_partial_download_path(dir_path).unwrap(); + assert_eq!(save_path, dir_path.join("fol der/file.tar.gz.part5")); + + // a partial download does not exist + let partial_exists = file_meta.partial_download_exists(dir_path).unwrap(); + assert!(partial_exists.is_none()); +} diff --git a/gday_file_transfer/tests/test_offer.rs b/gday_file_transfer/tests/test_offer.rs new file mode 100644 index 0000000..b0c1ddc --- /dev/null +++ b/gday_file_transfer/tests/test_offer.rs @@ -0,0 +1,129 @@ +use gday_file_transfer::{FileMetaLocal, FileOfferMsg, FileResponseMsg}; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +#[test] +fn test_file_offer() { + // create test directory + let temp_dir = tempfile::tempdir().unwrap(); + let dir_path = temp_dir.path(); + + let mut f = File::create_new(dir_path.join("completely_exists.tar.gz")).unwrap(); + write!(f, "--").unwrap(); + + let mut f = File::create_new(dir_path.join("completely_exists (1).tar.gz")).unwrap(); + write!(f, "---").unwrap(); + + let mut f = File::create_new(dir_path.join("wrong_size_exists.tar.gz")).unwrap(); + write!(f, "--").unwrap(); + + let mut f = File::create_new(dir_path.join("wrong_size_exists (1).tar.gz")).unwrap(); + write!(f, "---").unwrap(); + + let mut f = File::create_new(dir_path.join("just_partial.tar.gz.part9")).unwrap(); + write!(f, "----").unwrap(); + + let mut f = File::create_new(dir_path.join("partial_wrong_size.tar.gz.part6")).unwrap(); + write!(f, "----").unwrap(); + + let mut f = File::create_new(dir_path.join("exists_and_has_partial.tar.gz")).unwrap(); + write!(f, "----").unwrap(); + + let mut f = File::create_new(dir_path.join("exists_and_has_partial.tar.gz.part4")).unwrap(); + write!(f, "-").unwrap(); + + let sender_path = PathBuf::from("/random/example/"); + + let offer_files = vec![ + FileMetaLocal { + short_path: PathBuf::from("completely_exists.tar.gz"), + local_path: sender_path.join("completely_exists.tar.gz"), + len: 3, + }, + FileMetaLocal { + short_path: PathBuf::from("wrong_size_exists.tar.gz"), + local_path: sender_path.join("wrong_size_exists.tar.gz"), + len: 2, + }, + FileMetaLocal { + short_path: PathBuf::from("just_partial.tar.gz"), + local_path: sender_path.join("just_partial.tar.gz"), + len: 9, + }, + FileMetaLocal { + short_path: PathBuf::from("partial_wrong_size.tar.gz"), + local_path: sender_path.join("partial_wrong_size.tar.gz"), + len: 10, + }, + FileMetaLocal { + short_path: PathBuf::from("exists_and_has_partial.tar.gz"), + local_path: sender_path.join("exists_and_has_partial.tar.gz"), + len: 4, + }, + FileMetaLocal { + short_path: PathBuf::from("completely_unseen_file.tar.gz"), + local_path: sender_path.join("completely_unseen_file.tar.gz"), + len: 2, + }, + ]; + + let offer = FileOfferMsg::from(offer_files); + + let offered_size = offer.get_total_offered_size(); + assert_eq!(offered_size, 30); + + let accept_all = FileResponseMsg::accept_all_files(&offer); + assert_eq!( + accept_all.response, + vec![Some(0), Some(0), Some(0), Some(0), Some(0), Some(0)] + ); + assert_eq!(accept_all.get_num_fully_accepted(), 6); + assert_eq!(accept_all.get_num_partially_accepted(), 0); + assert_eq!(accept_all.get_num_not_rejected(), 6); + assert_eq!(offer.get_transfer_size(&accept_all).unwrap(), 30); + + let reject_all = FileResponseMsg::reject_all_files(&offer); + assert_eq!( + reject_all.response, + vec![None, None, None, None, None, None,] + ); + assert_eq!(reject_all.get_num_fully_accepted(), 0); + assert_eq!(reject_all.get_num_partially_accepted(), 0); + assert_eq!(reject_all.get_num_not_rejected(), 0); + assert_eq!(offer.get_transfer_size(&reject_all).unwrap(), 0); + + let only_new = FileResponseMsg::accept_only_full_new_files(&offer, dir_path).unwrap(); + assert_eq!( + only_new.response, + vec![None, Some(0), Some(0), Some(0), None, Some(0)] + ); + assert_eq!(only_new.get_num_fully_accepted(), 4); + assert_eq!(only_new.get_num_partially_accepted(), 0); + assert_eq!(only_new.get_num_not_rejected(), 4); + assert_eq!(offer.get_transfer_size(&only_new).unwrap(), 23); + + let only_remaining = FileResponseMsg::accept_only_remaining_portions(&offer, dir_path).unwrap(); + assert_eq!( + only_remaining.response, + vec![None, None, Some(4), None, Some(1), None] + ); + assert_eq!(only_remaining.get_num_fully_accepted(), 0); + assert_eq!(only_remaining.get_num_partially_accepted(), 2); + assert_eq!(only_remaining.get_num_not_rejected(), 2); + assert_eq!(offer.get_transfer_size(&only_remaining).unwrap(), 8); + + let only_new_and_interrupted = + FileResponseMsg::accept_only_new_and_interrupted(&offer, dir_path).unwrap(); + assert_eq!( + only_new_and_interrupted.response, + vec![None, Some(0), Some(4), Some(0), Some(1), Some(0)] + ); + assert_eq!(only_new_and_interrupted.get_num_fully_accepted(), 3); + assert_eq!(only_new_and_interrupted.get_num_partially_accepted(), 2); + assert_eq!(only_new_and_interrupted.get_num_not_rejected(), 5); + assert_eq!( + offer.get_transfer_size(&only_new_and_interrupted).unwrap(), + 22 + ); +} diff --git a/gday_hole_punch/tests/integration_test.rs b/gday_hole_punch/tests/integration_test.rs index 78e87be..992cf95 100644 --- a/gday_hole_punch/tests/integration_test.rs +++ b/gday_hole_punch/tests/integration_test.rs @@ -108,5 +108,6 @@ async fn test_integration() { .await .unwrap(); + // stop the server handle.abort(); } diff --git a/gday_server/tests/integration_test.rs b/gday_server/tests/integration_test.rs index 2574dd1..edc5f6d 100644 --- a/gday_server/tests/integration_test.rs +++ b/gday_server/tests/integration_test.rs @@ -151,6 +151,7 @@ async fn test_integration() { .await .unwrap(); + // stop the server handle.abort(); } @@ -202,5 +203,6 @@ async fn test_request_limit() { .await .unwrap(); + // stop the server handle.abort(); }