From f23beef347f6074de9af16f5b0f16e41264a3d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20B=C3=B6hme?= Date: Sun, 23 Feb 2025 16:37:10 +0100 Subject: [PATCH] feat: Integrate all components with new commands and comprehensive tests --- src/main.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index fcb5adf..deb2912 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,10 +4,9 @@ mod error; use std::collections::BTreeSet; use std::path::PathBuf; -use std::error::Error; use clap::{Parser, Subcommand}; use fs_err as fs; -use crate::error::{FileTagsError, ParseError}; +use crate::error::FileTagsError; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -18,17 +17,40 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// List all unique tags + /// List all unique tags found in files List { /// Files to process + #[arg(required = true, help = "One or more files to process")] files: Vec, }, /// Add tags to files Add { /// Tags to add - #[arg(long = "tag", required = true)] + #[arg(long = "tag", required = true, help = "Tags to add to files")] tags: Vec, /// Files to process + #[arg(required = true, help = "One or more files to add tags to")] + files: Vec, + }, + /// Remove tags from files + Remove { + /// Tags to remove + #[arg(long = "tag", required = true, help = "Tags to remove from files")] + tags: Vec, + /// Files to process + #[arg(required = true, help = "One or more files to remove tags from")] + files: Vec, + }, + /// Create a tag-based directory tree with symlinks + Tree { + /// Target directory for the tree + #[arg(long, required = true, help = "Target directory for creating the tree")] + dir: String, + /// Maximum depth of the tree + #[arg(long, default_value = "3", help = "Maximum depth of the directory tree")] + depth: usize, + /// Files to process + #[arg(required = true, help = "One or more files to create tree from")] files: Vec, }, } @@ -95,6 +117,75 @@ fn main() -> Result<(), FileTagsError> { add_tags_to_file(&file, &tags)?; } } + Commands::Remove { tags, files } => { + for file in files { + remove_tags_from_file(&file, &tags)?; + } + } + Commands::Tree { dir, depth, files } => { + create_tag_tree(&files, &dir, depth)?; + } + } + + Ok(()) +} + +fn remove_tags_from_file(file: &str, remove_tags: &[String]) -> Result<(), FileTagsError> { + let (base, current_tags, ext) = tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { + file: PathBuf::from(file), + source: e, + })?; + + let filtered_tags = tag_engine::filter_tags(current_tags, remove_tags); + let new_filename = tag_engine::serialize_tags(&base, &filtered_tags, &ext); + + // Preserve the original directory + let parent = std::path::Path::new(file).parent() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); + + let new_path = if parent.is_empty() { + new_filename + } else { + format!("{}/{}", parent, new_filename) + }; + + // Only rename if tags actually changed + if file != new_path { + fs::rename(file, &new_path).map_err(|e| FileTagsError::Rename { + from: PathBuf::from(file), + to: PathBuf::from(&new_path), + source: e, + })?; + } + + Ok(()) +} + +fn create_tag_tree(files: &[String], target_dir: &str, depth: usize) -> Result<(), FileTagsError> { + let target = PathBuf::from(target_dir); + + for file in files { + let (_base, tags, _ext) = tag_engine::parse_tags(file).map_err(|e| FileTagsError::Parse { + file: PathBuf::from(file), + source: e, + })?; + + // Create root symlink + let paths = vec![PathBuf::from(file)]; + symlink::create_symlink_tree(paths, &target)?; + + // Generate all tag combinations and create directory structure + let combinations = tag_engine::create_tag_combinations(&tags, depth); + for combo in combinations { + let mut dir_path = target.clone(); + for tag in &combo { + dir_path.push(tag); + } + + let paths = vec![PathBuf::from(file)]; + symlink::create_symlink_tree(paths, &dir_path)?; + } } Ok(()) @@ -209,4 +300,69 @@ mod tests { assert!(new_path.exists(), "Tagged file was not created in original directory"); Ok(()) } + + #[test] + fn test_remove_tags() -> Result<(), Box> { + let tmp_dir = TempDir::new()?; + let file = create_test_file(&tmp_dir, "test -- tag1 tag2 tag3.txt")?; + + remove_tags_from_file(&file, &vec!["tag2".to_string()])?; + + let new_path = tmp_dir.path().join("test -- tag1 tag3.txt"); + assert!(new_path.exists(), "File with removed tag not found"); + Ok(()) + } + + #[test] + fn test_create_tag_tree() -> Result<(), Box> { + let tmp_dir = TempDir::new()?; + let source = create_test_file(&tmp_dir, "test -- tag1 tag2.txt")?; + let tree_dir = tmp_dir.path().join("tree"); + + create_tag_tree(&[source], tree_dir.to_str().unwrap(), 2)?; + + // Check root symlink + assert!(tree_dir.join("test -- tag1 tag2.txt").exists()); + + // Check tag directories + assert!(tree_dir.join("tag1").join("test -- tag1 tag2.txt").exists()); + assert!(tree_dir.join("tag2").join("test -- tag1 tag2.txt").exists()); + assert!(tree_dir.join("tag1").join("tag2").join("test -- tag1 tag2.txt").exists()); + + Ok(()) + } + + #[test] + fn test_integration_all_commands() -> Result<(), Box> { + let tmp_dir = TempDir::new()?; + let file1 = create_test_file(&tmp_dir, "doc1.txt")?; + let file2 = create_test_file(&tmp_dir, "doc2.txt")?; + + // Add tags + add_tags_to_file(&file1, &vec!["work".to_string(), "draft".to_string()])?; + add_tags_to_file(&file2, &vec!["work".to_string(), "final".to_string()])?; + + // List tags + let tags = list_tags(&[ + tmp_dir.path().join("doc1 -- draft work.txt").to_str().unwrap().to_string(), + tmp_dir.path().join("doc2 -- final work.txt").to_str().unwrap().to_string(), + ])?; + assert_eq!(tags, vec!["draft", "final", "work"]); + + // Remove a tag + let file_to_remove = tmp_dir.path().join("doc1 -- draft work.txt").to_str().unwrap().to_string(); + remove_tags_from_file(&file_to_remove, &vec!["draft".to_string()])?; + + // Create tree + let tree_dir = tmp_dir.path().join("tree"); + create_tag_tree( + &[tmp_dir.path().join("doc1 -- work.txt").to_str().unwrap().to_string()], + tree_dir.to_str().unwrap(), + 2 + )?; + + assert!(tree_dir.join("work").join("doc1 -- work.txt").exists()); + + Ok(()) + } }