From f5c53d1e0e974d37934a0694cacd291853ace728 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 1 Dec 2025 12:29:32 -0500 Subject: [PATCH 1/3] Allow for pattern-based memories folder exclusion --- src/memories.rs | 51 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/src/memories.rs b/src/memories.rs index bf29b9d..c366e10 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -242,25 +242,52 @@ pub async fn list_memories( let base = Path::new(&app_state.base_path); - // Build a list of excluded directories with full paths - let excluded_paths: Vec = app_state - .excluded_dirs - .iter() - .map(|dir| base.join(dir)) - .collect(); + // Build a list of excluded directories and patterns, all scoped under base: + // - entries starting with '/' are treated as absolute *under base* (e.g. "/foo/bar" -> base/foo/bar) + // - entries without '/' are treated as substring patterns matched anywhere in the path + let mut excluded_dirs: Vec = Vec::new(); + let mut excluded_patterns: Vec = Vec::new(); - debug!("Excluded directories: {:?}", excluded_paths); + for dir in &app_state.excluded_dirs { + if dir.starts_with('/') { + // "Absolute under base": strip leading '/' and join with base + let rel = &dir[1..]; + if !rel.is_empty() { + excluded_dirs.push(base.join(rel)); + } + } else { + // Pure pattern (no '/'): match as substring anywhere in the path + excluded_patterns.push(dir.clone()); + } + } + + debug!("Excluded directories (under base): {:?}", excluded_dirs); + debug!("Excluded path patterns: {:?}", excluded_patterns); let entries: Vec<_> = WalkDir::new(base) .into_iter() .filter_map(|e| e.ok()) .filter(|e| { - // Skip excluded directories - if !excluded_paths.is_empty() { - let path = e.path(); - for excluded in &excluded_paths { + let path = e.path(); + + // Skip excluded directories (all are under base) + if !excluded_dirs.is_empty() { + for excluded in &excluded_dirs { if path.starts_with(excluded) { - debug!("Skipping excluded path: {:?}", path); + debug!("Skipping excluded dir path: {:?}", path); + return false; + } + } + } + + // Skip paths that match any of the relative patterns (substring match under base) + if !excluded_patterns.is_empty() { + if let Some(path_str) = path.to_str() { + if excluded_patterns.iter().any(|pat| path_str.contains(pat)) { + debug!( + "Skipping excluded pattern match: {:?} (patterns: {:?})", + path, excluded_patterns + ); return false; } } -- 2.49.1 From a7d065aadc3525500f3ce8dd75642995921681c3 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 1 Dec 2025 12:54:40 -0500 Subject: [PATCH 2/3] Tests and improved pattern-excluding behavior --- src/memories.rs | 264 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 218 insertions(+), 46 deletions(-) diff --git a/src/memories.rs b/src/memories.rs index c366e10..475d896 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -16,6 +16,83 @@ use crate::files::is_image_or_video; use crate::otel::{extract_context_from_request, global_tracer}; use crate::state::AppState; +// Helper that encapsulates path-exclusion semantics +#[derive(Debug)] +struct PathExcluder { + excluded_dirs: Vec, + excluded_patterns: Vec, +} + +impl PathExcluder { + /// Build from a `base` path and the raw exclusion entries. + /// + /// Rules: + /// - Entries starting with '/' are interpreted as "absolute under base" + /// (e.g. "/photos/private" -> base/photos/private). + /// - Entries without '/' are treated as substring patterns that match + /// anywhere in the full path string (still scoped under base). + fn new(base: &Path, raw_excluded: &[String]) -> Self { + let mut excluded_dirs = Vec::new(); + let mut excluded_patterns = Vec::new(); + + for dir in raw_excluded { + if dir.starts_with('/') { + // Absolute under base + let rel = &dir[1..]; + if !rel.is_empty() { + excluded_dirs.push(base.join(rel)); + } + } else { + // Pattern anywhere under base + excluded_patterns.push(dir.clone()); + } + } + + debug!( + "PathExcluder created. dirs={:?}, patterns={:?}", + excluded_dirs, excluded_patterns + ); + + Self { + excluded_dirs, + excluded_patterns, + } + } + + /// Returns true if `path` should be excluded. + fn is_excluded(&self, path: &Path) -> bool { + // Directory-based exclusions + for excluded in &self.excluded_dirs { + if path.starts_with(excluded) { + debug!("PathExcluder: excluded by dir: {:?} (rule: {:?})", path, excluded); + return true; + } + } + + // Pattern-based exclusions: match whole path components (dir or file name), + // not substrings. + if !self.excluded_patterns.is_empty() { + for component in path.components() { + if let Some(comp_str) = component.as_os_str().to_str() { + if self + .excluded_patterns + .iter() + .any(|pat| pat == comp_str) + { + debug!( + "PathExcluder: excluded by component pattern: {:?} (component: {:?}, patterns: {:?})", + path, comp_str, self.excluded_patterns + ); + return true; + } + } + } + } + + false + } +} + #[derive(Copy, Clone, Deserialize, PartialEq, Debug)] #[serde(rename_all = "lowercase")] pub enum MemoriesSpan { @@ -242,27 +319,8 @@ pub async fn list_memories( let base = Path::new(&app_state.base_path); - // Build a list of excluded directories and patterns, all scoped under base: - // - entries starting with '/' are treated as absolute *under base* (e.g. "/foo/bar" -> base/foo/bar) - // - entries without '/' are treated as substring patterns matched anywhere in the path - let mut excluded_dirs: Vec = Vec::new(); - let mut excluded_patterns: Vec = Vec::new(); - - for dir in &app_state.excluded_dirs { - if dir.starts_with('/') { - // "Absolute under base": strip leading '/' and join with base - let rel = &dir[1..]; - if !rel.is_empty() { - excluded_dirs.push(base.join(rel)); - } - } else { - // Pure pattern (no '/'): match as substring anywhere in the path - excluded_patterns.push(dir.clone()); - } - } - - debug!("Excluded directories (under base): {:?}", excluded_dirs); - debug!("Excluded path patterns: {:?}", excluded_patterns); + // Build the path excluder from base and env-configured exclusions + let path_excluder = PathExcluder::new(base, &app_state.excluded_dirs); let entries: Vec<_> = WalkDir::new(base) .into_iter() @@ -270,27 +328,9 @@ pub async fn list_memories( .filter(|e| { let path = e.path(); - // Skip excluded directories (all are under base) - if !excluded_dirs.is_empty() { - for excluded in &excluded_dirs { - if path.starts_with(excluded) { - debug!("Skipping excluded dir path: {:?}", path); - return false; - } - } - } - - // Skip paths that match any of the relative patterns (substring match under base) - if !excluded_patterns.is_empty() { - if let Some(path_str) = path.to_str() { - if excluded_patterns.iter().any(|pat| path_str.contains(pat)) { - debug!( - "Skipping excluded pattern match: {:?} (patterns: {:?})", - path, excluded_patterns - ); - return false; - } - } + // Skip paths that should be excluded + if path_excluder.is_excluded(path) { + return false; } true @@ -420,11 +460,9 @@ fn same_month_any_year(a: NaiveDate, b: NaiveDate) -> bool { mod tests { use super::*; use chrono::Timelike; - use std::fs::File; + use std::fs::{self, File}; use tempfile::tempdir; - // Add new tests for our date extraction functionality - #[test] fn test_extract_date_from_filename_screenshot_format() { let filename = "Screenshot_2014-06-01-20-44-50.png"; @@ -568,4 +606,138 @@ mod tests { let dt_modified = DateTime::::from_timestamp(modified.unwrap(), 0).unwrap(); assert_eq!(dt_modified.year(), today.year()); } -} + + #[test] + fn test_path_excluder_absolute_under_base() { + let tmp = tempdir().unwrap(); + let base = tmp.path(); + + // Simulate structure: + // base/photos/private/secret.jpg + // base/photos/public/ok.jpg + // base/screenshots/img.png + let photos_private = base.join("photos/private"); + let photos_public = base.join("photos/public"); + let screenshots = base.join("screenshots"); + + fs::create_dir_all(&photos_private).unwrap(); + fs::create_dir_all(&photos_public).unwrap(); + fs::create_dir_all(&screenshots).unwrap(); + + let secret = photos_private.join("secret.jpg"); + let ok = photos_public.join("ok.jpg"); + let shot = screenshots.join("img.png"); + + File::create(&secret).unwrap(); + File::create(&ok).unwrap(); + File::create(&shot).unwrap(); + + // Exclude "/photos/private" and "/screenshots" under base + let excluded = vec![ + String::from("/photos/private"), + String::from("/screenshots"), + ]; + let excluder = PathExcluder::new(base, &excluded); + + assert!(excluder.is_excluded(&secret), "secret should be excluded"); + assert!(excluder.is_excluded(&shot), "screenshots should be excluded"); + assert!( + !excluder.is_excluded(&ok), + "public photo should NOT be excluded" + ); + } + + #[test] + fn test_path_excluder_pattern_anywhere_under_base() { + let tmp = tempdir().unwrap(); + let base = tmp.path(); + + // Simulate: + // base/a/tmp_file.jpg + // base/b/normal.jpg + // base/c/sometmpdir/file.jpg + let a = base.join("a"); + let b = base.join("b"); + let c = base.join("c/tmp"); + + fs::create_dir_all(&a).unwrap(); + fs::create_dir_all(&b).unwrap(); + fs::create_dir_all(&c).unwrap(); + + let tmp_file = a.join("tmp_file.jpg"); + let normal = b.join("normal.jpg"); + let tmp_dir_file = c.join("file.jpg"); + + File::create(&tmp_file).unwrap(); + File::create(&normal).unwrap(); + File::create(&tmp_dir_file).unwrap(); + + // Exclude any path containing "tmp" + let excluded = vec![String::from("tmp")]; + let excluder = PathExcluder::new(base, &excluded); + + assert!( + !excluder.is_excluded(&tmp_file), + "file with 'tmp' in name should NOT be excluded" + ); + assert!( + excluder.is_excluded(&tmp_dir_file), + "file in directory with 'tmp' in path should be excluded" + ); + assert!( + !excluder.is_excluded(&normal), + "file without 'tmp' in its path should NOT be excluded" + ); + } + + #[test] + fn test_path_excluder_mixed_absolute_and_pattern() { + let tmp = tempdir().unwrap(); + let base = tmp.path(); + + // Simulate: + // base/photos/private/secret_tmp.jpg -> excluded by absolute dir rule + // base/photos/private/secret.jpg -> excluded by absolute dir rule + // base/photos/tmp/public.jpg -> excluded by pattern "tmp" (dir name) + // base/photos/public/tmp_public.jpg -> NOT excluded (file name contains "tmp" but not equal) + // base/other/keep.jpg -> NOT excluded + let photos_private = base.join("photos/private"); + let photos_tmp = base.join("photos/tmp"); + let photos_public = base.join("photos/public"); + let other = base.join("other"); + + fs::create_dir_all(&photos_private).unwrap(); + fs::create_dir_all(&photos_tmp).unwrap(); + fs::create_dir_all(&photos_public).unwrap(); + fs::create_dir_all(&other).unwrap(); + + let secret_tmp = photos_private.join("secret_tmp.jpg"); + let secret = photos_private.join("secret.jpg"); + let tmp_dir_file = photos_tmp.join("public.jpg"); + let tmp_in_name = photos_public.join("tmp_public.jpg"); + let keep = other.join("keep.jpg"); + + File::create(&secret_tmp).unwrap(); + File::create(&secret).unwrap(); + File::create(&tmp_dir_file).unwrap(); + File::create(&tmp_in_name).unwrap(); + File::create(&keep).unwrap(); + + // Mixed: exclude "/photos/private" (dir) and any component equal to "tmp" + let excluded = vec![String::from("/photos/private"), String::from("tmp")]; + let excluder = PathExcluder::new(base, &excluded); + + // Entire private tree is excluded by dir rule + assert!(excluder.is_excluded(&secret_tmp)); + assert!(excluder.is_excluded(&secret)); + + // Dir 'tmp' under photos excluded by pattern + assert!(excluder.is_excluded(&tmp_dir_file)); + + // File name containing 'tmp' but not equal should NOT be excluded + assert!(!excluder.is_excluded(&tmp_in_name)); + + // keep.jpg doesn't match any rule + assert!(!excluder.is_excluded(&keep)); + } + } -- 2.49.1 From f02a8583684f30b25c54cf23de4904694729357a Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 1 Dec 2025 13:03:28 -0500 Subject: [PATCH 3/3] Bump to 0.3.1 and format/clippy --- Cargo.lock | 4 +- Cargo.toml | 2 +- src/database/mod.rs | 6 +- src/files.rs | 7 +- src/main.rs | 2 +- src/memories.rs | 286 ++++++++++++++++++++++---------------------- src/tags.rs | 6 +- src/video/actors.rs | 2 +- src/video/ffmpeg.rs | 4 +- 9 files changed, 156 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 85d337e..5accb47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "actix" @@ -1534,7 +1534,7 @@ dependencies = [ [[package]] name = "image-api" -version = "0.3.0" +version = "0.3.1" dependencies = [ "actix", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index 70471c1..736dfa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "image-api" -version = "0.3.0" +version = "0.3.1" authors = ["Cameron Cordes "] edition = "2024" diff --git a/src/database/mod.rs b/src/database/mod.rs index 55071b4..ed6f884 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -88,12 +88,10 @@ impl UserDao for SqliteUserDao { fn user_exists(&mut self, user: &str) -> bool { use schema::users::dsl::*; - users + !users .filter(username.eq(user)) .load::(&mut self.connection) - .unwrap_or_default() - .first() - .is_some() + .unwrap_or_default().is_empty() } } diff --git a/src/files.rs b/src/files.rs index 6c77afc..bc8eb9b 100644 --- a/src/files.rs +++ b/src/files.rs @@ -64,8 +64,8 @@ pub async fn list_photos( let span_context = opentelemetry::Context::current_with_span(span); let search_recursively = req.recursive.unwrap_or(false); - if let Some(tag_ids) = &req.tag_ids { - if search_recursively { + if let Some(tag_ids) = &req.tag_ids + && search_recursively { let filter_mode = &req.tag_filter_mode.unwrap_or(FilterMode::Any); info!( "Searching for tags: {}. With path: '{}' and filter mode: {:?}", @@ -142,7 +142,6 @@ pub async fn list_photos( .into_http_internal_err() .unwrap_or_else(|e| e.error_response()); } - } match file_system.get_files_for_path(search_path) { Ok(files) => { @@ -221,7 +220,7 @@ pub async fn list_photos( let dirs = files .iter() - .filter(|&f| f.metadata().map_or(false, |md| md.is_dir())) + .filter(|&f| f.metadata().is_ok_and(|md| md.is_dir())) .map(|path: &PathBuf| { let relative = path.strip_prefix(&app_state.base_path).unwrap(); relative.to_path_buf() diff --git a/src/main.rs b/src/main.rs index adf6728..1cc859e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -200,7 +200,7 @@ async fn upload_image( while let Some(Ok(data)) = part.next().await { file_content.put(data); } - } else if content_type.get_name().map_or(false, |name| name == "path") { + } else if content_type.get_name() == Some("path") { while let Some(Ok(data)) = part.next().await { if let Ok(path) = std::str::from_utf8(&data) { file_path = Some(path.to_string()) diff --git a/src/memories.rs b/src/memories.rs index 475d896..e0ab6f3 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -36,9 +36,8 @@ impl PathExcluder { let mut excluded_patterns = Vec::new(); for dir in raw_excluded { - if dir.starts_with('/') { + if let Some(rel) = dir.strip_prefix('/') { // Absolute under base - let rel = &dir[1..]; if !rel.is_empty() { excluded_dirs.push(base.join(rel)); } @@ -64,7 +63,10 @@ impl PathExcluder { // Directory-based exclusions for excluded in &self.excluded_dirs { if path.starts_with(excluded) { - debug!("PathExcluder: excluded by dir: {:?} (rule: {:?})", path, excluded); + debug!( + "PathExcluder: excluded by dir: {:?} (rule: {:?})", + path, excluded + ); return true; } } @@ -73,19 +75,14 @@ impl PathExcluder { // not substrings. if !self.excluded_patterns.is_empty() { for component in path.components() { - if let Some(comp_str) = component.as_os_str().to_str() { - if self - .excluded_patterns - .iter() - .any(|pat| pat == comp_str) - { + if let Some(comp_str) = component.as_os_str().to_str() + && self.excluded_patterns.iter().any(|pat| pat == comp_str) { debug!( "PathExcluder: excluded by component pattern: {:?} (component: {:?}, patterns: {:?})", path, comp_str, self.excluded_patterns ); return true; } - } } } @@ -255,20 +252,19 @@ fn extract_date_from_filename(filename: &str) -> Option> { let timestamp_str = captures.get(1)?.as_str(); // Millisecond timestamp (13 digits) - if timestamp_str.len() >= 13 { - if let Some(date_time) = timestamp_str[0..13] + if timestamp_str.len() >= 13 + && let Some(date_time) = timestamp_str[0..13] .parse::() .ok() - .and_then(|timestamp_millis| DateTime::from_timestamp_millis(timestamp_millis)) + .and_then(DateTime::from_timestamp_millis) .map(|naive_dt| naive_dt.fixed_offset()) { return Some(date_time); } - } // Second timestamp (10 digits) - if timestamp_str.len() >= 10 { - if let Some(date_time) = timestamp_str[0..10] + if timestamp_str.len() >= 10 + && let Some(date_time) = timestamp_str[0..10] .parse::() .ok() .and_then(|timestamp_secs| DateTime::from_timestamp(timestamp_secs, 0)) @@ -276,7 +272,6 @@ fn extract_date_from_filename(filename: &str) -> Option> { { return Some(date_time); } - } } None @@ -607,137 +602,140 @@ mod tests { assert_eq!(dt_modified.year(), today.year()); } - #[test] - fn test_path_excluder_absolute_under_base() { - let tmp = tempdir().unwrap(); - let base = tmp.path(); + #[test] + fn test_path_excluder_absolute_under_base() { + let tmp = tempdir().unwrap(); + let base = tmp.path(); - // Simulate structure: - // base/photos/private/secret.jpg - // base/photos/public/ok.jpg - // base/screenshots/img.png - let photos_private = base.join("photos/private"); - let photos_public = base.join("photos/public"); - let screenshots = base.join("screenshots"); + // Simulate structure: + // base/photos/private/secret.jpg + // base/photos/public/ok.jpg + // base/screenshots/img.png + let photos_private = base.join("photos/private"); + let photos_public = base.join("photos/public"); + let screenshots = base.join("screenshots"); - fs::create_dir_all(&photos_private).unwrap(); - fs::create_dir_all(&photos_public).unwrap(); - fs::create_dir_all(&screenshots).unwrap(); + fs::create_dir_all(&photos_private).unwrap(); + fs::create_dir_all(&photos_public).unwrap(); + fs::create_dir_all(&screenshots).unwrap(); - let secret = photos_private.join("secret.jpg"); - let ok = photos_public.join("ok.jpg"); - let shot = screenshots.join("img.png"); + let secret = photos_private.join("secret.jpg"); + let ok = photos_public.join("ok.jpg"); + let shot = screenshots.join("img.png"); - File::create(&secret).unwrap(); - File::create(&ok).unwrap(); - File::create(&shot).unwrap(); + File::create(&secret).unwrap(); + File::create(&ok).unwrap(); + File::create(&shot).unwrap(); - // Exclude "/photos/private" and "/screenshots" under base - let excluded = vec![ - String::from("/photos/private"), - String::from("/screenshots"), - ]; - let excluder = PathExcluder::new(base, &excluded); + // Exclude "/photos/private" and "/screenshots" under base + let excluded = vec![ + String::from("/photos/private"), + String::from("/screenshots"), + ]; + let excluder = PathExcluder::new(base, &excluded); - assert!(excluder.is_excluded(&secret), "secret should be excluded"); - assert!(excluder.is_excluded(&shot), "screenshots should be excluded"); - assert!( - !excluder.is_excluded(&ok), - "public photo should NOT be excluded" - ); - } - - #[test] - fn test_path_excluder_pattern_anywhere_under_base() { - let tmp = tempdir().unwrap(); - let base = tmp.path(); - - // Simulate: - // base/a/tmp_file.jpg - // base/b/normal.jpg - // base/c/sometmpdir/file.jpg - let a = base.join("a"); - let b = base.join("b"); - let c = base.join("c/tmp"); - - fs::create_dir_all(&a).unwrap(); - fs::create_dir_all(&b).unwrap(); - fs::create_dir_all(&c).unwrap(); - - let tmp_file = a.join("tmp_file.jpg"); - let normal = b.join("normal.jpg"); - let tmp_dir_file = c.join("file.jpg"); - - File::create(&tmp_file).unwrap(); - File::create(&normal).unwrap(); - File::create(&tmp_dir_file).unwrap(); - - // Exclude any path containing "tmp" - let excluded = vec![String::from("tmp")]; - let excluder = PathExcluder::new(base, &excluded); - - assert!( - !excluder.is_excluded(&tmp_file), - "file with 'tmp' in name should NOT be excluded" - ); - assert!( - excluder.is_excluded(&tmp_dir_file), - "file in directory with 'tmp' in path should be excluded" - ); - assert!( - !excluder.is_excluded(&normal), - "file without 'tmp' in its path should NOT be excluded" - ); - } - - #[test] - fn test_path_excluder_mixed_absolute_and_pattern() { - let tmp = tempdir().unwrap(); - let base = tmp.path(); - - // Simulate: - // base/photos/private/secret_tmp.jpg -> excluded by absolute dir rule - // base/photos/private/secret.jpg -> excluded by absolute dir rule - // base/photos/tmp/public.jpg -> excluded by pattern "tmp" (dir name) - // base/photos/public/tmp_public.jpg -> NOT excluded (file name contains "tmp" but not equal) - // base/other/keep.jpg -> NOT excluded - let photos_private = base.join("photos/private"); - let photos_tmp = base.join("photos/tmp"); - let photos_public = base.join("photos/public"); - let other = base.join("other"); - - fs::create_dir_all(&photos_private).unwrap(); - fs::create_dir_all(&photos_tmp).unwrap(); - fs::create_dir_all(&photos_public).unwrap(); - fs::create_dir_all(&other).unwrap(); - - let secret_tmp = photos_private.join("secret_tmp.jpg"); - let secret = photos_private.join("secret.jpg"); - let tmp_dir_file = photos_tmp.join("public.jpg"); - let tmp_in_name = photos_public.join("tmp_public.jpg"); - let keep = other.join("keep.jpg"); - - File::create(&secret_tmp).unwrap(); - File::create(&secret).unwrap(); - File::create(&tmp_dir_file).unwrap(); - File::create(&tmp_in_name).unwrap(); - File::create(&keep).unwrap(); - - // Mixed: exclude "/photos/private" (dir) and any component equal to "tmp" - let excluded = vec![String::from("/photos/private"), String::from("tmp")]; - let excluder = PathExcluder::new(base, &excluded); - - // Entire private tree is excluded by dir rule - assert!(excluder.is_excluded(&secret_tmp)); - assert!(excluder.is_excluded(&secret)); - - // Dir 'tmp' under photos excluded by pattern - assert!(excluder.is_excluded(&tmp_dir_file)); - - // File name containing 'tmp' but not equal should NOT be excluded - assert!(!excluder.is_excluded(&tmp_in_name)); - - // keep.jpg doesn't match any rule - assert!(!excluder.is_excluded(&keep)); - } + assert!(excluder.is_excluded(&secret), "secret should be excluded"); + assert!( + excluder.is_excluded(&shot), + "screenshots should be excluded" + ); + assert!( + !excluder.is_excluded(&ok), + "public photo should NOT be excluded" + ); } + + #[test] + fn test_path_excluder_pattern_anywhere_under_base() { + let tmp = tempdir().unwrap(); + let base = tmp.path(); + + // Simulate: + // base/a/tmp_file.jpg + // base/b/normal.jpg + // base/c/sometmpdir/file.jpg + let a = base.join("a"); + let b = base.join("b"); + let c = base.join("c/tmp"); + + fs::create_dir_all(&a).unwrap(); + fs::create_dir_all(&b).unwrap(); + fs::create_dir_all(&c).unwrap(); + + let tmp_file = a.join("tmp_file.jpg"); + let normal = b.join("normal.jpg"); + let tmp_dir_file = c.join("file.jpg"); + + File::create(&tmp_file).unwrap(); + File::create(&normal).unwrap(); + File::create(&tmp_dir_file).unwrap(); + + // Exclude any path containing "tmp" + let excluded = vec![String::from("tmp")]; + let excluder = PathExcluder::new(base, &excluded); + + assert!( + !excluder.is_excluded(&tmp_file), + "file with 'tmp' in name should NOT be excluded" + ); + assert!( + excluder.is_excluded(&tmp_dir_file), + "file in directory with 'tmp' in path should be excluded" + ); + assert!( + !excluder.is_excluded(&normal), + "file without 'tmp' in its path should NOT be excluded" + ); + } + + #[test] + fn test_path_excluder_mixed_absolute_and_pattern() { + let tmp = tempdir().unwrap(); + let base = tmp.path(); + + // Simulate: + // base/photos/private/secret_tmp.jpg -> excluded by absolute dir rule + // base/photos/private/secret.jpg -> excluded by absolute dir rule + // base/photos/tmp/public.jpg -> excluded by pattern "tmp" (dir name) + // base/photos/public/tmp_public.jpg -> NOT excluded (file name contains "tmp" but not equal) + // base/other/keep.jpg -> NOT excluded + let photos_private = base.join("photos/private"); + let photos_tmp = base.join("photos/tmp"); + let photos_public = base.join("photos/public"); + let other = base.join("other"); + + fs::create_dir_all(&photos_private).unwrap(); + fs::create_dir_all(&photos_tmp).unwrap(); + fs::create_dir_all(&photos_public).unwrap(); + fs::create_dir_all(&other).unwrap(); + + let secret_tmp = photos_private.join("secret_tmp.jpg"); + let secret = photos_private.join("secret.jpg"); + let tmp_dir_file = photos_tmp.join("public.jpg"); + let tmp_in_name = photos_public.join("tmp_public.jpg"); + let keep = other.join("keep.jpg"); + + File::create(&secret_tmp).unwrap(); + File::create(&secret).unwrap(); + File::create(&tmp_dir_file).unwrap(); + File::create(&tmp_in_name).unwrap(); + File::create(&keep).unwrap(); + + // Mixed: exclude "/photos/private" (dir) and any component equal to "tmp" + let excluded = vec![String::from("/photos/private"), String::from("tmp")]; + let excluder = PathExcluder::new(base, &excluded); + + // Entire private tree is excluded by dir rule + assert!(excluder.is_excluded(&secret_tmp)); + assert!(excluder.is_excluded(&secret)); + + // Dir 'tmp' under photos excluded by pattern + assert!(excluder.is_excluded(&tmp_dir_file)); + + // File name containing 'tmp' but not equal should NOT be excluded + assert!(!excluder.is_excluded(&tmp_in_name)); + + // keep.jpg doesn't match any rule + assert!(!excluder.is_excluded(&keep)); + } +} diff --git a/src/tags.rs b/src/tags.rs index f3c8c6d..22bbcf9 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -504,12 +504,10 @@ impl TagDao for SqliteTagDao { trace_db_call(context, "query", "get_files_with_any_tags", |_| { use diesel::dsl::*; // Create the placeholders for the IN clauses - let tag_placeholders = std::iter::repeat("?") - .take(tag_ids.len()) + let tag_placeholders = std::iter::repeat_n("?", tag_ids.len()) .collect::>() .join(","); - let exclude_placeholders = std::iter::repeat("?") - .take(exclude_tag_ids.len()) + let exclude_placeholders = std::iter::repeat_n("?", exclude_tag_ids.len()) .collect::>() .join(","); diff --git a/src/video/actors.rs b/src/video/actors.rs index c58c665..7902158 100644 --- a/src/video/actors.rs +++ b/src/video/actors.rs @@ -293,7 +293,7 @@ impl Handler for PlaylistGenerator { .stderr(Stdio::piped()) .output() .inspect_err(|e| error!("Failed to run ffmpeg on child process: {}", e)) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) + .map_err(|e| std::io::Error::other(e.to_string())) .await; // Hang on to the permit until we're done decoding and then explicitly drop diff --git a/src/video/ffmpeg.rs b/src/video/ffmpeg.rs index 4722115..0e708c2 100644 --- a/src/video/ffmpeg.rs +++ b/src/video/ffmpeg.rs @@ -34,7 +34,7 @@ impl Ffmpeg { .stderr(Stdio::piped()) .output() .inspect_err(|e| error!("Failed to run ffmpeg on child process: {}", e)) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) + .map_err(|e| std::io::Error::other(e.to_string())) .await; if let Ok(ref res) = ffmpeg_result { @@ -58,7 +58,7 @@ impl Ffmpeg { duration .parse::() .map(|duration| duration as u32) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) + .map_err(|e| std::io::Error::other(e.to_string())) }) .inspect(|duration| debug!("Found video duration: {:?}", duration)) } -- 2.49.1