use super::{generate::Generate, sync_down::SyncDown, sync_up::SyncUp}; use crate::configuration::Configuration; use crate::constants::METADATA_FILENAME; use crate::metadata::Metadata; use serde_json; use std::fs::{copy, create_dir_all, read_dir, remove_dir_all, write}; use std::io::{Error, ErrorKind, Result}; use std::path::{Path, PathBuf}; pub struct Update; impl Update { pub fn new() -> Self { Update } fn copy_post(source: &PathBuf, target: &Path) -> Result<()> { let post_name = source .file_name() .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "Could not get post filename."))?; let target_post = target.join(post_name); copy(source, target_post)?; Ok(()) } fn write_metadata(metadata: &Metadata, metadata_location: &PathBuf) -> Result<()> { let serialized_metadata = serde_json::to_string(&metadata)?; write(metadata_location, serialized_metadata)?; Ok(()) } fn archive(source: &PathBuf, target: &Path) -> Result<()> { let entries = read_dir(source)?; for entry in entries { let entry = entry?; let entry_type = entry.file_type()?; let entry_name = entry.file_name(); let entry_source = entry.path(); let entry_target = target.join(entry_name); if entry_type.is_dir() { create_dir_all(&entry_target)?; Update::archive(&entry_source, &entry_target)?; } else { copy(&entry_source, &entry_target)?; } } Ok(()) } } impl super::Command for Update { fn before_dependencies(&self) -> Vec> { vec![Box::new(SyncDown::new())] } fn execute( &self, input: Option<&String>, configuration: &Configuration, _: &str, ) -> Result<()> { let input = input.ok_or_else(|| { Error::new(ErrorKind::InvalidInput, "You must provide a path to a post") })?; let post_location = PathBuf::from(input); if !post_location.exists() { return Err(Error::new( ErrorKind::NotFound, "The path provided does not exist", )); } // Step 1. Write into the ephemeral posts create_dir_all(&configuration.posts_directory)?; let first_post_path = configuration.posts_directory.join("0"); let metadata_file_path = first_post_path.join(METADATA_FILENAME); let metadata = Metadata::read_or_create(&metadata_file_path); let _ = remove_dir_all(&first_post_path); create_dir_all(&first_post_path)?; Update::copy_post(&post_location, &first_post_path)?; Update::write_metadata(&metadata, &metadata_file_path)?; // Step 2. Write into the archive create_dir_all(&configuration.archive_directory)?; let post_archive_path = configuration.archive_directory.join(metadata.id); let _ = remove_dir_all(&post_archive_path); create_dir_all(&post_archive_path)?; Update::archive(&first_post_path, &post_archive_path)?; Ok(()) } fn after_dependencies(&self) -> Vec> { vec![Box::new(Generate::new()), Box::new(SyncUp::new())] } fn command(&self) -> &'static str { "update" } fn help(&self) -> &'static str { "\t\tUpdates latest blog post" } } #[cfg(test)] mod tests { use std::fs::{create_dir_all, read_dir}; use std::path::PathBuf; use super::*; use crate::command::Command; use crate::configuration::Configuration; use test_utilities::*; #[test] fn test_update_command() { let update = Update::new(); // Create all directories let test_dir = setup_test_dir(); // Reference but don't create the directories let posts_dir = test_dir.join("posts"); let archive_dir = test_dir.join("archive"); // Create configuration let mut configuration = Configuration::new(); configuration.posts_directory = posts_dir.clone(); configuration.archive_directory = archive_dir.clone(); // Create Test File let test_file = test_dir.join("cool-test-file-😎.gmi"); let test_file_string = test_file.display().to_string(); create_test_file(&test_file, "# I try."); // Update 1 of 2 update .execute(Some(&test_file_string), &configuration, "update") .expect("Could not update a .gmi"); assert!(&posts_dir.join("0").exists()); assert_file_contents(&posts_dir.join("0/cool-test-file-😎.gmi"), "# I try."); let entries = read_dir(&archive_dir).expect("Could not read archive"); let mut found_path = PathBuf::new(); for entry in entries { let entry = entry.expect("Could not read entry"); found_path = entry.path(); assert_file_contents(&found_path.join("cool-test-file-😎.gmi"), "# I try."); assert!(&found_path.join("metadata.json").exists()); } // Create Second Test File let second_test_file = test_dir.join("cooler-test-file-😎.gmi"); let second_test_file_string = second_test_file.display().to_string(); create_test_file(&second_test_file, "# I REALLY try."); // Update 2 of 2 update .execute(Some(&second_test_file_string), &configuration, "update") .expect("Could not update a .gmi"); assert_file_contents( &posts_dir.join("0/cooler-test-file-😎.gmi"), "# I REALLY try.", ); assert_file_contents( &found_path.join("cooler-test-file-😎.gmi"), "# I REALLY try.", ); cleanup_test_dir(&test_dir); } #[test] fn test_write_metadata_fail() { // Create all directories let test_dir = setup_test_dir(); // Reference but don't create the directories let metadata_dir = test_dir.join("metadata/nested/cannot/write"); // Create metadata let metadata = Metadata { id: "1234".to_string(), created_on: 1234, }; // Update 1 of 2 let result = Update::write_metadata(&metadata, &metadata_dir); assert!(result.is_err()); cleanup_test_dir(&test_dir); } #[test] fn test_recursive_archive() { // Create all directories let test_dir = setup_test_dir(); // Create the directories let source_dir = test_dir.join("source"); let target_dir = test_dir.join("target"); let nested_dir = source_dir.join("nested"); create_dir_all(&nested_dir).expect("Could not create source dir."); create_dir_all(&target_dir).expect("Could not create target dir."); // Create the two test files. let test_file = source_dir.join("not_nested.gmi"); let nested_test_file = nested_dir.join("very_much_nested.gmi"); create_test_file(&test_file, "# Unlike a Bird"); create_test_file(&nested_test_file, "# Very much like a Bird"); Update::archive(&source_dir, &target_dir).expect("Could not archive the test directories"); assert_file_contents(&target_dir.join("not_nested.gmi"), "# Unlike a Bird"); assert_file_contents( &target_dir.join("nested/very_much_nested.gmi"), "# Very much like a Bird", ); cleanup_test_dir(&test_dir); } #[test] fn test_update_fails_on_directory() { let update = Update::new(); // Create all directories let test_dir = setup_test_dir(); // Reference but don't create the directories let nested_dir = test_dir.join("nested"); let posts_dir = test_dir.join("posts"); let archive_dir = test_dir.join("archive"); create_dir_all(&nested_dir).expect("Could not create nested dir."); // Create configuration let mut configuration = Configuration::new(); configuration.posts_directory = posts_dir.clone(); configuration.archive_directory = archive_dir.clone(); // Create Test File let test_file = nested_dir.join("cool-test-file-😎.gmi"); create_test_file(&test_file, "# I try."); let nested_string = nested_dir.display().to_string(); let result = update.execute(Some(&nested_string), &configuration, "update"); assert!(result.is_err()); cleanup_test_dir(&test_dir); } #[test] fn test_update_fails_on_missing_input() { let update = Update::new(); // Create all directories let test_dir = setup_test_dir(); // Reference but don't create the directories let posts_dir = test_dir.join("posts"); let archive_dir = test_dir.join("archive"); // Create configuration let mut configuration = Configuration::new(); configuration.posts_directory = posts_dir.clone(); configuration.archive_directory = archive_dir.clone(); // Add 1 of 3 let result = update.execute(None, &configuration, "update"); assert!(result.is_err()); cleanup_test_dir(&test_dir); } #[test] fn test_update_fails_if_file_does_not_exist() { let update = Update::new(); // Create all directories let test_dir = setup_test_dir(); // Reference but don't create the directories let posts_dir = test_dir.join("posts"); let archive_dir = test_dir.join("archive"); // Create configuration let mut configuration = Configuration::new(); configuration.posts_directory = posts_dir.clone(); configuration.archive_directory = archive_dir.clone(); // Create Test File let test_file = test_dir.join("cool-secret-test-file-😎.gmi"); let test_file_string = test_file.display().to_string(); // Add 1 of 3 let result = update.execute(Some(&test_file_string), &configuration, "update"); assert!(result.is_err()); cleanup_test_dir(&test_dir); } #[test] fn update_before_dependencies() { let update = Update::new(); let dependencies = update.before_dependencies(); assert_eq!(dependencies.len(), 1); assert_eq!(dependencies[0].command(), "sync-down"); } #[test] fn update_after_dependencies() { let update = Update::new(); let dependencies = update.after_dependencies(); assert_eq!(dependencies.len(), 2); assert_eq!(dependencies[0].command(), "generate"); assert_eq!(dependencies[1].command(), "sync-up"); } // These two tests feel pointless but I'm doing it for the coverage :p #[test] fn update_command_output() { let update = Update::new(); update.command(); } #[test] fn update_help_output() { let update = Update::new(); update.help(); } }