feat: Integrate all components with new commands and comprehensive tests
This commit is contained in:
parent
4517fa34f8
commit
f23beef347
1 changed files with 160 additions and 4 deletions
164
src/main.rs
164
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<String>,
|
||||
},
|
||||
/// 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<String>,
|
||||
/// Files to process
|
||||
#[arg(required = true, help = "One or more files to add tags to")]
|
||||
files: Vec<String>,
|
||||
},
|
||||
/// Remove tags from files
|
||||
Remove {
|
||||
/// Tags to remove
|
||||
#[arg(long = "tag", required = true, help = "Tags to remove from files")]
|
||||
tags: Vec<String>,
|
||||
/// Files to process
|
||||
#[arg(required = true, help = "One or more files to remove tags from")]
|
||||
files: Vec<String>,
|
||||
},
|
||||
/// 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<String>,
|
||||
},
|
||||
}
|
||||
|
|
@ -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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue