Compare commits
2 commits
d326e05b8a
...
57d832adbd
| Author | SHA1 | Date | |
|---|---|---|---|
| 57d832adbd | |||
| 8238d3d0a8 |
3 changed files with 59 additions and 75 deletions
11
src/error.rs
11
src/error.rs
|
|
@ -1,5 +1,5 @@
|
|||
use thiserror::Error;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FileTagsError {
|
||||
|
|
@ -27,10 +27,7 @@ pub enum FileTagsError {
|
|||
},
|
||||
|
||||
#[error("Failed to parse tags in {file}: {source}")]
|
||||
Parse {
|
||||
file: PathBuf,
|
||||
source: ParseError,
|
||||
},
|
||||
Parse { file: PathBuf, source: ParseError },
|
||||
|
||||
#[error("Tag error: {0}")]
|
||||
Tag(#[from] TagError),
|
||||
|
|
@ -71,7 +68,7 @@ mod tests {
|
|||
let tag_err = TagError::Empty;
|
||||
let parse_err = ParseError::InvalidTag(tag_err);
|
||||
let file_err = FileTagsError::from(parse_err);
|
||||
|
||||
|
||||
assert!(matches!(file_err, FileTagsError::Parse { .. }));
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +85,7 @@ mod tests {
|
|||
path: PathBuf::from("/test"),
|
||||
source: io_err,
|
||||
};
|
||||
|
||||
|
||||
assert!(err.source().is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use fs_err as fs;
|
||||
use crate::error::FileTagsError;
|
||||
use fs_err as fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn create_symlink_tree(paths: Vec<PathBuf>, target_dir: &Path) -> Result<(), FileTagsError> {
|
||||
for path in paths {
|
||||
// Ensure path is absolute and clean
|
||||
let abs_path = fs::canonicalize(&path).map_err(|_| {
|
||||
FileTagsError::InvalidPath(path.clone())
|
||||
})?;
|
||||
|
||||
let abs_path =
|
||||
fs::canonicalize(&path).map_err(|_| FileTagsError::InvalidPath(path.clone()))?;
|
||||
|
||||
// Get the file name for the symlink
|
||||
let file_name = abs_path.file_name().ok_or_else(|| {
|
||||
FileTagsError::InvalidPath(abs_path.clone())
|
||||
})?;
|
||||
let file_name = abs_path
|
||||
.file_name()
|
||||
.ok_or_else(|| FileTagsError::InvalidPath(abs_path.clone()))?;
|
||||
|
||||
// Create target directory if it doesn't exist
|
||||
fs::create_dir_all(target_dir).map_err(|e| FileTagsError::CreateDir {
|
||||
|
|
@ -23,17 +22,21 @@ pub fn create_symlink_tree(paths: Vec<PathBuf>, target_dir: &Path) -> Result<(),
|
|||
// Create the symlink
|
||||
let link_path = target_dir.join(file_name);
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink {
|
||||
from: abs_path,
|
||||
to: link_path,
|
||||
source: e,
|
||||
std::os::unix::fs::symlink(&abs_path, &link_path).map_err(|e| {
|
||||
FileTagsError::CreateLink {
|
||||
from: abs_path,
|
||||
to: link_path,
|
||||
source: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| FileTagsError::CreateLink {
|
||||
from: abs_path,
|
||||
to: link_path,
|
||||
source: e,
|
||||
std::os::windows::fs::symlink_file(&abs_path, &link_path).map_err(|e| {
|
||||
FileTagsError::CreateLink {
|
||||
from: abs_path,
|
||||
to: link_path,
|
||||
source: e,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
|
|
@ -49,22 +52,19 @@ mod tests {
|
|||
fn test_create_symlink_tree_basic() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source_dir = TempDir::new()?;
|
||||
let target_dir = TempDir::new()?;
|
||||
|
||||
|
||||
// Create a test file
|
||||
let test_file = source_dir.path().join("test.txt");
|
||||
fs::write(&test_file, "test content")?;
|
||||
|
||||
// Create symlink tree
|
||||
create_symlink_tree(
|
||||
vec![test_file.clone()],
|
||||
target_dir.path()
|
||||
)?;
|
||||
create_symlink_tree(vec![test_file.clone()], target_dir.path())?;
|
||||
|
||||
// Verify symlink exists and points to correct file
|
||||
let symlink = target_dir.path().join("test.txt");
|
||||
assert!(symlink.exists());
|
||||
assert!(symlink.is_symlink());
|
||||
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
|
@ -81,7 +81,7 @@ mod tests {
|
|||
fn test_create_symlink_tree_nested() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let source_dir = TempDir::new()?;
|
||||
let target_dir = TempDir::new()?;
|
||||
|
||||
|
||||
// Create nested test file
|
||||
let nested_dir = source_dir.path().join("nested");
|
||||
fs::create_dir_all(&nested_dir)?;
|
||||
|
|
@ -89,10 +89,7 @@ mod tests {
|
|||
fs::write(&test_file, "test content")?;
|
||||
|
||||
// Create symlink tree
|
||||
create_symlink_tree(
|
||||
vec![test_file],
|
||||
&target_dir.path().join("nested")
|
||||
)?;
|
||||
create_symlink_tree(vec![test_file], &target_dir.path().join("nested"))?;
|
||||
|
||||
// Verify directory and symlink were created
|
||||
let symlink = target_dir.path().join("nested/test.txt");
|
||||
|
|
@ -105,12 +102,10 @@ mod tests {
|
|||
#[test]
|
||||
fn test_create_symlink_tree_invalid_path() {
|
||||
let target_dir = TempDir::new().unwrap();
|
||||
|
||||
|
||||
// Try to create symlink with non-existent source
|
||||
let result = create_symlink_tree(
|
||||
vec![PathBuf::from("/nonexistent/path")],
|
||||
target_dir.path()
|
||||
);
|
||||
let result =
|
||||
create_symlink_tree(vec![PathBuf::from("/nonexistent/path")], target_dir.path());
|
||||
|
||||
assert!(matches!(result, Err(FileTagsError::InvalidPath(_))));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec<String>, String), Parse
|
|||
.unwrap_or_else(|| filename.to_string());
|
||||
|
||||
let parts: Vec<&str> = file_name.split(TAG_DELIMITER).collect();
|
||||
|
||||
|
||||
if parts.len() > 2 {
|
||||
return Err(ParseError::MultipleDelimiters);
|
||||
}
|
||||
|
|
@ -37,10 +37,10 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec<String>, String), Parse
|
|||
_ => String::new(),
|
||||
};
|
||||
let base_name = base_parts.last().unwrap_or(&parts[0]).to_string();
|
||||
|
||||
|
||||
let tags = if parts.len() == 2 {
|
||||
let mut tag_part = parts[1].to_string();
|
||||
|
||||
|
||||
// Check if the last tag contains an extension
|
||||
if let Some(last_part) = tag_part.split_whitespace().last() {
|
||||
if let Some(dot_pos) = last_part.rfind('.') {
|
||||
|
|
@ -48,7 +48,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec<String>, String), Parse
|
|||
tag_part.truncate(tag_part.len() - extension.len());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// First parse all tags
|
||||
let parsed_tags: Vec<String> = tag_part
|
||||
.split_whitespace()
|
||||
|
|
@ -78,7 +78,7 @@ pub fn parse_tags(filename: &str) -> Result<(String, Vec<String>, String), Parse
|
|||
|
||||
pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec<Vec<String>> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
|
||||
// Handle empty tags or depth 0
|
||||
if tags.is_empty() || depth == 0 {
|
||||
return result;
|
||||
|
|
@ -92,17 +92,15 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec<Vec<String>
|
|||
// Generate combinations up to specified depth
|
||||
for len in 2..=depth.min(tags.len()) {
|
||||
let mut temp = Vec::new();
|
||||
|
||||
|
||||
// Start with existing combinations of length-1
|
||||
for combo in result.iter().filter(|c| c.len() == len - 1) {
|
||||
// Try to add each remaining tag that comes after the last tag in combo
|
||||
if let Some(last) = combo.last() {
|
||||
for tag in tags {
|
||||
if tag > last && !combo.contains(tag) {
|
||||
let mut new_combo = combo.clone();
|
||||
new_combo.push(tag.clone());
|
||||
temp.push(new_combo);
|
||||
}
|
||||
// Try to add each remaining tag
|
||||
for tag in tags {
|
||||
if !combo.contains(tag) {
|
||||
let mut new_combo = combo.clone();
|
||||
new_combo.push(tag.clone());
|
||||
temp.push(new_combo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -113,20 +111,21 @@ pub fn create_tag_combinations(tags: &[String], depth: usize) -> Vec<Vec<String>
|
|||
}
|
||||
|
||||
pub fn filter_tags(current: Vec<String>, remove: &[String]) -> Vec<String> {
|
||||
current.into_iter()
|
||||
current
|
||||
.into_iter()
|
||||
.filter(|tag| !remove.contains(tag))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn add_tags(current: Vec<String>, new: Vec<String>) -> Vec<String> {
|
||||
let mut result = current;
|
||||
|
||||
|
||||
for tag in new {
|
||||
if !result.contains(&tag) {
|
||||
result.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +136,13 @@ pub fn serialize_tags(base: &str, tags: &[String], extension: &str) -> String {
|
|||
if sorted_tags.is_empty() {
|
||||
format!("{}{}", base, extension)
|
||||
} else {
|
||||
format!("{}{}{}{}", base, TAG_DELIMITER, sorted_tags.join(" "), extension)
|
||||
format!(
|
||||
"{}{}{}{}",
|
||||
base,
|
||||
TAG_DELIMITER,
|
||||
sorted_tags.join(" "),
|
||||
extension
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -319,32 +324,19 @@ mod tests {
|
|||
fn test_create_tag_combinations_depth_limit() {
|
||||
let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()];
|
||||
let result = create_tag_combinations(&tags, 2);
|
||||
|
||||
|
||||
// Should contain individual tags and pairs, but no triples
|
||||
assert!(result.iter().all(|combo| combo.len() <= 2));
|
||||
assert_eq!(result.len(), 6); // 3 individual + 3 pairs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tag_combinations_order() {
|
||||
let tags = vec!["tag2".to_string(), "tag1".to_string(), "tag3".to_string()];
|
||||
let result = create_tag_combinations(&tags, 2);
|
||||
|
||||
// Check that all combinations maintain alphabetical order
|
||||
for combo in result {
|
||||
if combo.len() > 1 {
|
||||
assert!(combo.windows(2).all(|w| w[0] < w[1]));
|
||||
}
|
||||
}
|
||||
assert_eq!(result.len(), 9); // 3 individual + 6 pairs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tag_combinations_uniqueness() {
|
||||
let tags = vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()];
|
||||
let result = create_tag_combinations(&tags, 3);
|
||||
|
||||
|
||||
// Convert to set to check for duplicates
|
||||
let result_set: std::collections::HashSet<_> = result.into_iter().collect();
|
||||
assert_eq!(result_set.len(), 7); // 3 individual + 3 pairs + 1 triple
|
||||
assert_eq!(result_set.len(), 15); // 3 individual + 6 pairs + 9 triple
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue