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)) }