falcon_library/
import_check.rs1use std::path::{Path, PathBuf};
16
17use crate::duplicates::content_hash;
18use crate::error::{Error, Result};
19use crate::library::Library;
20
21pub const UNCHECKED: &[&str] = &["svg", "tif", "tiff", "bmp", "tga", "txt"];
23
24pub fn is_checked(ext: &str) -> bool {
26 !ext.is_empty() && !UNCHECKED.contains(&ext)
27}
28
29pub fn same_file(path: &Path, candidates: &[(String, PathBuf)]) -> Option<String> {
31 if candidates.is_empty() {
32 return None;
33 }
34 let hash = content_hash(path).ok()?;
35 candidates.iter().find(|(_, p)| content_hash(p).is_ok_and(|h| h == hash)).map(|(id, _)| id.clone())
36}
37
38impl Library {
39 pub fn duplicate_candidates(&self, size: u64, ext: &str) -> Vec<(String, PathBuf)> {
43 if !is_checked(ext) {
44 return Vec::new();
45 }
46 self.items()
47 .iter()
48 .filter(|i| !i.is_deleted && i.size == size && i.ext.eq_ignore_ascii_case(ext))
49 .map(|i| (i.id.clone(), i.original_path(self.root())))
50 .collect()
51 }
52
53 pub fn use_existing_for_import(&mut self, existing: &str, folders: &[String]) -> Result<()> {
60 let Some(mut item) = self.item(existing).cloned() else {
61 return Err(Error::Import { path: self.root().to_path_buf(), reason: format!("no item {existing}") });
62 };
63 let known = self.folder_ids();
64 for f in folders.iter().filter(|f| known.contains(f.as_str())) {
65 crate::autotag::join_folder(&mut item, &self.metadata.folders, f);
66 }
67 item.modification_time = self.next_import_time(crate::edit::now_ms());
68 self.update_item(item)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::import::{ImportOptions, stage};
76
77 #[test]
78 fn unchecked_formats() {
79 assert!(is_checked("png") && is_checked("mp4") && is_checked("ttf"));
80 for ext in ["svg", "tif", "tiff", "bmp", "tga", "txt", ""] {
81 assert!(!is_checked(ext), "{ext}");
82 }
83 }
84
85 #[test]
86 fn finds_and_uses_the_existing_copy() {
87 let tmp = tempfile::tempdir().unwrap();
88 let mut lib = Library::create(tmp.path().join("D.library")).unwrap();
89 let folder = lib.create_folder(None, "F").unwrap();
90 lib.set_folder_auto_tags(&folder, &["auto".into()]).unwrap();
91 let png = |name: &str, shade: u8| {
92 let p = tmp.path().join(name);
93 image::RgbImage::from_pixel(8, 8, image::Rgb([shade, 0, 0])).save(&p).unwrap();
94 p
95 };
96 let opts = ImportOptions { palette: false, tags: vec!["mine".into()], ..Default::default() };
97 let a = lib.import_file(png("a.png", 10), &opts).unwrap();
98 let old_time = lib.item(&a).unwrap().modification_time;
99
100 let into = ImportOptions { folders: vec![folder.clone()], tags: vec!["new".into()], ..opts.clone() };
102 let copy = stage(png("copy.png", 10), &into).unwrap();
103 let cands = lib.duplicate_candidates(copy.item().size, ©.item().ext);
104 assert_eq!(cands.len(), 1);
105 assert_eq!(same_file(©.original(), &cands), Some(a.clone()));
106
107 let other = stage(png("other.png", 200), &into).unwrap();
109 assert_eq!(same_file(&other.original(), &cands), None);
110 assert!(lib.duplicate_candidates(copy.item().size, "jpg").is_empty());
112 assert!(lib.duplicate_candidates(copy.item().size, "svg").is_empty());
113
114 let before = lib.items().len();
115 lib.use_existing_for_import(&a, &into.folders).unwrap();
116 let item = lib.item(&a).unwrap();
117 assert_eq!(item.folders, std::slice::from_ref(&folder));
118 assert_eq!(item.tags, ["mine", "auto"], "the folder's auto-tags, not the import's tags");
119 assert!(item.modification_time > old_time);
120 assert_eq!(lib.items().len(), before);
121 drop(copy);
122
123 lib.set_trashed(std::slice::from_ref(&a), true).unwrap();
124 assert!(lib.duplicate_candidates(lib.item(&a).unwrap().size, "png").is_empty());
125 assert!(lib.use_existing_for_import("nope", &[]).is_err());
126 }
127}