1use std::collections::{HashMap, HashSet};
9use std::fs;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use crate::error::{Error, Result};
13use crate::import::sanitize_name;
14use crate::library::{Library, write_json};
15use crate::ids;
16use crate::model::{Comment, Folder, FolderOrder, ICON_COLORS, Item, SavedFilter, TagGroup};
17
18pub const HISTORY_LIMIT: usize = 120;
20
21impl Library {
22 pub fn update_item(&mut self, mut item: Item) -> Result<()> {
24 let old = self.item(&item.id).cloned().ok_or_else(|| Error::Import {
25 path: self.root().to_path_buf(),
26 reason: format!("no item {}", item.id),
27 })?;
28 if item.name != old.name {
29 item.name = sanitize_name(&item.name, &item.ext);
30 }
31 if item.name != old.name {
32 let root = self.root().to_path_buf();
33 let (from, to) = (old.original_path(&root), item.original_path(&root));
34 fs::rename(&from, &to).map_err(|e| Error::io(&from, e))?;
35 if let Some(thumb) = old.thumbnail_path(&root) {
36 let dest = thumb.with_file_name(item.thumbnail_name());
37 fs::rename(&thumb, &dest).map_err(|e| Error::io(&thumb, e))?;
38 }
39 }
40 item.last_modified = now_ms().max(old.last_modified + 1);
41 self.register_item(item)
42 }
43
44 pub fn add_comment(&mut self, item_id: &str, mut comment: Comment) -> Result<String> {
49 let mut item = self.item(item_id).cloned().ok_or_else(|| self.invalid(format!("no item {item_id}")))?;
50 if comment.id.is_empty() {
51 comment.id = ids::new_id();
52 }
53 comment.last_modified.get_or_insert_with(now_ms);
54 let id = comment.id.clone();
55 let mut list = item.comments();
56 match comment.duration.as_ref().and_then(|d| d.as_f64()) {
57 Some(t) => {
58 let at = list.iter().position(|c| c.duration.as_ref().and_then(|d| d.as_f64()).is_some_and(|d| d > t)).unwrap_or(list.len());
59 list.insert(at, comment);
60 }
61 None => list.push(comment),
62 }
63 item.set_comments(&list);
64 self.update_item(item)?;
65 Ok(id)
66 }
67
68 pub fn update_comment(&mut self, item_id: &str, comment: Comment) -> Result<()> {
72 let mut item = self.item(item_id).cloned().ok_or_else(|| self.invalid(format!("no item {item_id}")))?;
73 let mut list = item.comments();
74 let slot = list.iter_mut().find(|c| c.id == comment.id).ok_or_else(|| self.invalid(format!("no comment {}", comment.id)))?;
75 *slot = comment;
76 item.set_comments(&list);
77 self.update_item(item)
78 }
79
80 pub fn delete_comment(&mut self, item_id: &str, comment_id: &str) -> Result<()> {
82 let mut item = self.item(item_id).cloned().ok_or_else(|| self.invalid(format!("no item {item_id}")))?;
83 let mut list = item.comments();
84 let before = list.len();
85 list.retain(|c| c.id != comment_id);
86 if list.len() == before {
87 return Err(self.invalid(format!("no comment {comment_id}")));
88 }
89 item.set_comments(&list);
90 self.update_item(item)
91 }
92
93 pub fn set_trashed(&mut self, ids: &[String], trashed: bool) -> Result<()> {
96 for id in ids {
97 let Some(mut item) = self.item(id).cloned() else { continue };
98 item.is_deleted = trashed;
99 if trashed {
100 item.extra.insert("deletedTime".into(), now_ms().into());
101 }
102 self.update_item(item)?;
103 }
104 Ok(())
105 }
106
107 pub fn delete_permanently(&mut self, ids: &[String]) -> Result<()> {
110 for id in ids {
111 let Some(item) = self.item(id).cloned() else { continue };
112 let dir = item.info_dir(self.root());
113 if dir.is_dir() {
114 fs::remove_dir_all(&dir).map_err(|e| Error::io(&dir, e))?;
115 }
116 self.forget_item(id)?;
117 }
118 Ok(())
119 }
120
121 pub fn empty_trash(&mut self) -> Result<usize> {
124 let ids: Vec<String> = self.items().iter().filter(|i| i.is_deleted).map(|i| i.id.clone()).collect();
125 self.delete_permanently(&ids)?;
126 Ok(ids.len())
127 }
128
129 pub fn rename_tag(&mut self, from: &str, to: &str) -> Result<usize> {
134 let to = to.trim();
135 if to.is_empty() || from == to {
136 return Ok(0);
137 }
138 let affected: Vec<Item> = self.items().iter().filter(|i| i.tags.iter().any(|t| t == from)).cloned().collect();
139 let n = affected.len();
140 for mut item in affected {
141 let mut tags = Vec::with_capacity(item.tags.len());
142 for t in item.tags.drain(..) {
143 let t = if t == from { to.to_string() } else { t };
144 if !tags.contains(&t) {
145 tags.push(t);
146 }
147 }
148 item.tags = tags;
149 self.update_item(item)?;
150 }
151 let swap = |list: &mut Vec<String>| {
154 if list.iter().any(|t| t == to) {
155 list.retain(|t| t != from);
156 } else {
157 for t in list.iter_mut() {
158 if t == from {
159 *t = to.to_string();
160 }
161 }
162 let mut seen = std::collections::HashSet::new();
163 list.retain(|t| seen.insert(t.clone()));
164 }
165 };
166 swap(&mut self.tags.history_tags);
167 swap(&mut self.tags.starred_tags);
168 for g in &mut self.metadata.tags_groups {
169 swap(&mut g.tags);
170 }
171 let rename = |list: &mut Vec<String>| {
175 if let Some(i) = list.iter().position(|t| t == from) {
176 list[i] = to.to_string();
177 let mut seen = HashSet::new();
178 list.retain(|t| seen.insert(t.clone()));
179 }
180 };
181 walk_folders_mut(&mut self.metadata.folders, &mut |f| rename(&mut f.tags));
182 for sf in &mut self.metadata.smart_folders {
183 rename_in_smart_rules(sf, from, to);
184 }
185 self.save_metadata()?;
186 self.save_tags()?;
187 Ok(n)
188 }
189
190 pub fn delete_tag(&mut self, tag: &str) -> Result<usize> {
193 let affected: Vec<Item> = self.items().iter().filter(|i| i.tags.iter().any(|t| t == tag)).cloned().collect();
194 let n = affected.len();
195 for mut item in affected {
196 item.tags.retain(|t| t != tag);
197 self.update_item(item)?;
198 }
199 self.tags.history_tags.retain(|t| t != tag);
200 self.tags.starred_tags.retain(|t| t != tag);
201 for g in &mut self.metadata.tags_groups {
202 g.tags.retain(|t| t != tag);
203 }
204 walk_folders_mut(&mut self.metadata.folders, &mut |f| f.tags.retain(|t| t != tag));
207 self.save_metadata()?;
208 self.save_tags()?;
209 Ok(n)
210 }
211
212 pub fn set_tag_starred(&mut self, tag: &str, starred: bool) -> Result<()> {
214 self.tags.starred_tags.retain(|t| t != tag);
215 if starred {
216 self.tags.starred_tags.insert(0, tag.to_string());
217 }
218 self.save_tags()
219 }
220
221 pub fn record_tag_use(&mut self, tags: &[String]) -> Result<()> {
224 for t in tags.iter().rev() {
225 self.tags.history_tags.retain(|h| h != t);
226 self.tags.history_tags.insert(0, t.clone());
227 }
228 self.tags.history_tags.truncate(HISTORY_LIMIT);
229 self.save_tags()
230 }
231}
232
233fn walk_folders_mut(folders: &mut [Folder], f: &mut impl FnMut(&mut Folder)) {
235 for folder in folders {
236 f(folder);
237 walk_folders_mut(&mut folder.children, f);
238 }
239}
240
241fn rename_in_smart_rules(entry: &mut serde_json::Value, from: &str, to: &str) {
246 use serde_json::Value;
247 if let Some(conditions) = entry.get_mut("conditions").and_then(Value::as_array_mut) {
248 for c in conditions {
249 let Some(rules) = c.get_mut("rules").and_then(Value::as_array_mut) else { continue };
250 for r in rules {
251 if r.get("property").and_then(Value::as_str) != Some("tags") {
252 continue;
253 }
254 let Some(list) = r.get_mut("value").and_then(Value::as_array_mut) else { continue };
255 if let Some(i) = list.iter().position(|v| v.as_str() == Some(from)) {
256 list[i] = Value::String(to.to_string());
257 let mut seen: Vec<Value> = Vec::with_capacity(list.len());
258 list.retain(|v| {
259 if seen.contains(v) {
260 false
261 } else {
262 seen.push(v.clone());
263 true
264 }
265 });
266 }
267 }
268 }
269 }
270 if let Some(children) = entry.get_mut("children").and_then(Value::as_array_mut) {
271 for c in children {
272 rename_in_smart_rules(c, from, to);
273 }
274 }
275}
276
277impl Library {
281 pub fn create_tag_group(&mut self, name: &str, tags: &[String], color: Option<&str>) -> Result<String> {
285 self.check_color(color)?;
286 let id = ids::new_id();
287 let mut list = Vec::new();
288 for t in tags.iter().map(|t| clean_tag(t)) {
289 if !list.contains(&t) {
290 list.push(t);
291 }
292 }
293 self.metadata.tags_groups.push(TagGroup {
294 id: id.clone(),
295 name: display_name(name),
296 tags: list,
297 color: color.map(str::to_string),
298 description: None,
299 extra: Default::default(),
300 });
301 self.save_metadata()?;
302 Ok(id)
303 }
304
305 pub fn rename_tag_group(&mut self, id: &str, name: &str) -> Result<()> {
307 self.tag_group_mut(id)?.name = display_name(name);
308 self.save_metadata()
309 }
310
311 pub fn set_tag_group_tags(&mut self, id: &str, tags: &[String]) -> Result<()> {
314 let g = self.tag_group_mut(id)?;
315 g.tags.clear();
316 for t in tags.iter().map(|t| clean_tag(t)) {
317 if !g.tags.contains(&t) {
318 g.tags.push(t);
319 }
320 }
321 self.save_metadata()
322 }
323
324 pub fn add_tags_to_group(&mut self, id: &str, tags: &[String]) -> Result<()> {
327 self.tag_group_mut(id)?;
328 let tags: Vec<String> = tags.iter().map(|t| clean_tag(t)).collect();
329 for g in &mut self.metadata.tags_groups {
330 if g.id == id {
331 for t in &tags {
332 if !g.tags.contains(t) {
333 g.tags.push(t.clone());
334 }
335 }
336 } else {
337 g.tags.retain(|t| !tags.contains(t));
338 }
339 }
340 self.save_metadata()
341 }
342
343 pub fn remove_tags_from_group(&mut self, id: &str, tags: &[String]) -> Result<()> {
345 self.tag_group_mut(id)?.tags.retain(|t| !tags.contains(t));
346 self.save_metadata()
347 }
348
349 pub fn set_tag_group_color(&mut self, id: &str, color: Option<&str>) -> Result<()> {
351 self.check_color(color)?;
352 self.tag_group_mut(id)?.color = color.map(str::to_string);
353 self.save_metadata()
354 }
355
356 pub fn set_tag_group_description(&mut self, id: &str, description: &str) -> Result<()> {
360 self.tag_group_mut(id)?.description = Some(description.to_string());
361 self.save_metadata()
362 }
363
364 pub fn delete_tag_group(&mut self, id: &str, remove_tags: bool) -> Result<()> {
367 let pos = self.tag_group_index(id)?;
368 let group = self.metadata.tags_groups.remove(pos);
369 self.save_metadata()?;
370 if remove_tags {
371 for t in &group.tags {
372 self.delete_tag(t)?;
373 }
374 }
375 Ok(())
376 }
377
378 pub fn reorder_tag_group(&mut self, id: &str, index: usize) -> Result<()> {
381 let pos = self.tag_group_index(id)?;
382 let g = self.metadata.tags_groups.remove(pos);
383 let at = index.min(self.metadata.tags_groups.len());
384 self.metadata.tags_groups.insert(at, g);
385 self.save_metadata()
386 }
387
388 fn tag_group_index(&self, id: &str) -> Result<usize> {
389 self.metadata.tags_groups.iter().position(|g| g.id == id).ok_or_else(|| self.invalid(format!("no tag group {id}")))
390 }
391
392 fn tag_group_mut(&mut self, id: &str) -> Result<&mut TagGroup> {
393 let pos = self.tag_group_index(id)?;
394 Ok(&mut self.metadata.tags_groups[pos])
395 }
396}
397
398impl Library {
402 pub fn create_saved_filter(&mut self, name: &str, rule: serde_json::Value) -> Result<usize> {
405 if name.is_empty() {
406 return Err(self.invalid("empty saved filter name"));
407 }
408 self.saved_filters.push(SavedFilter { name: name.to_string(), rule, extra: Default::default() });
409 self.save_filters()?;
410 Ok(self.saved_filters.len() - 1)
411 }
412
413 pub fn rename_saved_filter(&mut self, index: usize, name: &str) -> Result<()> {
415 if name.is_empty() {
416 return Err(self.invalid("empty saved filter name"));
417 }
418 self.saved_filter_mut(index)?.name = name.to_string();
419 self.save_filters()
420 }
421
422 pub fn update_saved_filter(&mut self, index: usize, rule: serde_json::Value) -> Result<()> {
424 self.saved_filter_mut(index)?.rule = rule;
425 self.save_filters()
426 }
427
428 pub fn delete_saved_filter(&mut self, index: usize) -> Result<()> {
430 self.saved_filter_mut(index)?;
431 self.saved_filters.remove(index);
432 self.save_filters()
433 }
434
435 pub fn reorder_saved_filter(&mut self, index: usize, to: usize) -> Result<()> {
438 self.saved_filter_mut(index)?;
439 let f = self.saved_filters.remove(index);
440 let at = to.min(self.saved_filters.len());
441 self.saved_filters.insert(at, f);
442 self.save_filters()
443 }
444
445 fn saved_filter_mut(&mut self, index: usize) -> Result<&mut SavedFilter> {
446 let err = self.invalid(format!("no saved filter {index}"));
447 self.saved_filters.get_mut(index).ok_or(err)
448 }
449
450 pub(crate) fn save_filters(&mut self) -> Result<()> {
453 self.journal_saved_filters();
454 if self.batching(|d| d.saved_filters = true) {
455 return Ok(());
456 }
457 write_json(&self.root().join("saved-filters.json"), &self.saved_filters)
458 }
459}
460
461#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
463pub struct SubfolderSort {
464 pub descending: bool,
466 pub all_levels: bool,
468}
469
470fn clean_tag(t: &str) -> String {
472 t.replace(['\r', '\n'], "")
473}
474
475pub(crate) fn display_name(name: &str) -> String {
478 use unicode_normalization::UnicodeNormalization;
479 name.chars().take(1024).nfc().collect()
480}
481
482impl Library {
483 pub fn create_folder(&mut self, parent: Option<&str>, name: &str) -> Result<String> {
487 let id = ids::new_id();
488 let mut folder = Folder::new(id.clone(), display_name(name), now_ms());
489 match parent {
490 None => self.metadata.folders.push(folder),
491 Some(p) => match find_mut(&mut self.metadata.folders, p) {
492 Some(f) => {
493 folder.icon.clone_from(&f.icon);
494 folder.icon_color.clone_from(&f.icon_color);
495 f.children.push(folder)
496 }
497 None => return Err(self.missing(p)),
498 },
499 }
500 self.save_metadata()?;
501 Ok(id)
502 }
503
504 pub fn rename_folder(&mut self, id: &str, name: &str) -> Result<()> {
508 let Some(f) = find_mut(&mut self.metadata.folders, id) else { return Err(self.missing(id)) };
509 f.name = display_name(name);
510 f.pinyin = None;
511 self.save_metadata()
512 }
513
514 pub fn set_folder_order(&mut self, id: &str, order: Option<FolderOrder>, increase: bool) -> Result<()> {
517 let Some(f) = find_mut(&mut self.metadata.folders, id) else { return Err(self.missing(id)) };
518 f.order_by = order.map(|o| o.as_str().to_string());
519 f.sort_increase = order.map(|_| increase);
520 self.save_metadata()
521 }
522
523 pub fn set_folder_icon(&mut self, id: &str, icon: Option<&str>, color: Option<&str>) -> Result<()> {
527 self.check_color(color)?;
528 let Some(f) = find_mut(&mut self.metadata.folders, id) else { return Err(self.missing(id)) };
529 f.icon = icon.map(str::to_string);
530 f.icon_color = color.map(str::to_string);
531 self.save_metadata()
532 }
533
534 pub fn set_folder_cover(&mut self, id: &str, item: Option<&str>) -> Result<()> {
536 let Some(f) = find_mut(&mut self.metadata.folders, id) else { return Err(self.missing(id)) };
537 f.cover_id = item.map(str::to_string);
538 self.save_metadata()
539 }
540
541 pub fn set_folder_description(&mut self, id: &str, description: &str) -> Result<()> {
543 let Some(f) = find_mut(&mut self.metadata.folders, id) else { return Err(self.missing(id)) };
544 f.description = description.to_string();
545 self.save_metadata()
546 }
547
548 pub fn set_folder_password(&mut self, id: &str, password: Option<(&str, &str)>) -> Result<()> {
553 let encoded = match password {
554 Some(("", _)) => return Err(self.invalid("empty password")),
555 Some((p, hint)) => Some((encode_password(p).ok_or_else(|| self.invalid("password has characters beyond Latin-1"))?, hint)),
556 None => None,
557 };
558 let Some(f) = find_mut(&mut self.metadata.folders, id) else { return Err(self.missing(id)) };
559 let (password, tips) = encoded.unwrap_or_default();
560 f.password = password;
561 f.password_tips = tips.to_string();
562 self.save_metadata()
563 }
564
565 pub fn verify_folder_password(&self, id: &str, password: &str) -> bool {
568 self.all_folders()
569 .into_iter()
570 .find(|f| f.id == id)
571 .is_some_and(|f| f.is_locked() && encode_password(password).is_some_and(|e| e == f.password))
572 }
573
574 pub fn delete_folder(&mut self, id: &str) -> Result<()> {
577 self.delete_folders(&[id.to_string()], true)
578 }
579
580 pub fn delete_folders(&mut self, ids: &[String], trash_items: bool) -> Result<()> {
588 let known = self.folder_ids();
589 if let Some(id) = ids.iter().find(|id| !known.contains(id.as_str())) {
590 return Err(self.missing(id));
591 }
592 let mut gone = HashSet::new();
593 for id in ids {
594 if let Some(removed) = remove(&mut self.metadata.folders, id) {
595 removed.walk(&mut |f| {
596 gone.insert(f.id.clone());
597 });
598 }
599 }
600 self.metadata
601 .quick_access
602 .retain(|q| q.get("id").and_then(|v| v.as_str()).is_none_or(|i| !gone.contains(i)));
603 self.save_metadata()?;
604 let affected: Vec<Item> = self
605 .items()
606 .iter()
607 .filter(|i| i.folders.iter().any(|f| gone.contains(f)))
608 .cloned()
609 .collect();
610 for mut item in affected {
611 item.folders.retain(|f| !gone.contains(f));
612 if trash_items && item.folders.is_empty() {
613 item.is_deleted = true;
614 }
615 self.update_item(item)?;
616 }
617 Ok(())
618 }
619
620 pub fn duplicate_folder(&mut self, id: &str) -> Result<String> {
626 fn fresh(f: &mut Folder, now: u64) {
627 f.id = ids::new_id();
628 f.modification_time = now;
629 f.password = String::new();
630 f.password_tips = String::new();
631 for c in &mut f.children {
632 fresh(c, now);
633 }
634 }
635 let parent = parent_map(&self.metadata.folders).get(id).copied().ok_or_else(|| self.missing(id))?.map(str::to_string);
636 let mut copy = find_mut(&mut self.metadata.folders, id).expect("found above").clone();
637 fresh(&mut copy, now_ms());
638 let new_id = copy.id.clone();
639 let siblings = match parent {
640 None => &mut self.metadata.folders,
641 Some(p) => &mut find_mut(&mut self.metadata.folders, &p).expect("parent exists").children,
642 };
643 let at = siblings.iter().position(|f| f.id == id).expect("in its parent");
644 siblings.insert(at + 1, copy);
645 self.save_metadata()?;
646 Ok(new_id)
647 }
648
649 pub fn rename_folders(&mut self, names: &[(String, String)]) -> Result<()> {
654 let known = self.folder_ids();
655 if let Some((id, _)) = names.iter().find(|(id, _)| !known.contains(id.as_str())) {
656 return Err(self.missing(id));
657 }
658 for (id, name) in names {
659 let f = find_mut(&mut self.metadata.folders, id).expect("checked above");
660 f.name = display_name(name);
661 f.pinyin = None;
662 }
663 self.save_metadata()
664 }
665
666 pub fn restyle_folders(&mut self, ids: &[String], icon: Option<Option<&str>>, color: Option<Option<&str>>) -> Result<()> {
670 self.check_color(color.flatten())?;
671 self.expect_folders(ids)?;
672 for id in ids {
673 let f = find_mut(&mut self.metadata.folders, id).expect("checked above");
674 if let Some(i) = icon {
675 f.icon = i.map(str::to_string);
676 }
677 if let Some(c) = color {
678 f.icon_color = c.map(str::to_string);
679 }
680 }
681 self.save_metadata()
682 }
683
684 pub fn set_folders_order(&mut self, ids: &[String], order: Option<FolderOrder>) -> Result<()> {
689 self.expect_folders(ids)?;
690 for id in ids {
691 let f = find_mut(&mut self.metadata.folders, id).expect("checked above");
692 f.order_by = order.map(|o| o.as_str().to_string());
693 f.sort_increase = order.map(|_| f.sort_increase.unwrap_or(true));
694 }
695 self.save_metadata()
696 }
697
698 pub fn set_folders_increase(&mut self, ids: &[String], increase: bool) -> Result<()> {
700 self.expect_folders(ids)?;
701 for id in ids {
702 find_mut(&mut self.metadata.folders, id).expect("checked above").sort_increase = Some(increase);
703 }
704 self.save_metadata()
705 }
706
707 pub fn move_folders(&mut self, ids: &[String], target: &str, place: FolderPlace) -> Result<()> {
715 self.expect_folders(ids)?;
716 if !self.folder_ids().contains(target) {
717 return Err(self.missing(target));
718 }
719 let path = folder_path(&self.metadata.folders, target);
720 if ids.iter().any(|id| path.contains(&id.as_str())) {
721 return Err(self.invalid("cannot move a folder into itself"));
722 }
723 let moved = self.take_folders(ids);
724 let (siblings, at) = match place {
725 FolderPlace::Into => {
726 let kids = &mut find_mut(&mut self.metadata.folders, target).expect("checked above").children;
727 let end = kids.len();
728 (kids, end)
729 }
730 FolderPlace::Above | FolderPlace::Below => {
731 let parent = parent_map(&self.metadata.folders)[target].map(str::to_string);
732 let list = match parent {
733 None => &mut self.metadata.folders,
734 Some(p) => &mut find_mut(&mut self.metadata.folders, &p).expect("parent exists").children,
735 };
736 let i = list.iter().position(|f| f.id == target).expect("target is in its parent");
737 (list, if place == FolderPlace::Below { i + 1 } else { i })
738 }
739 };
740 siblings.splice(at..at, moved);
741 self.save_metadata()
742 }
743
744 pub fn move_folders_to_top(&mut self, ids: &[String]) -> Result<()> {
747 self.expect_folders(ids)?;
748 let moved = self.take_folders(ids);
749 self.metadata.folders.extend(moved);
750 self.save_metadata()
751 }
752
753 fn take_folders(&mut self, ids: &[String]) -> Vec<Folder> {
756 let picked: HashSet<&str> = ids.iter().map(String::as_str).collect();
757 let parents = parent_map(&self.metadata.folders);
758 let movers: Vec<String> = self
759 .all_folders()
760 .into_iter()
761 .filter(|f| picked.contains(f.id.as_str()) && !parents[f.id.as_str()].is_some_and(|p| picked.contains(p)))
762 .map(|f| f.id.clone())
763 .collect();
764 movers.iter().map(|id| remove(&mut self.metadata.folders, id).expect("checked by the caller")).collect()
765 }
766
767 pub fn move_folder(&mut self, id: &str, parent: Option<&str>) -> Result<()> {
771 self.reorder_folder(id, parent, usize::MAX)
772 }
773
774 pub fn reorder_folder(&mut self, id: &str, new_parent: Option<&str>, index: usize) -> Result<()> {
780 let Some(folder) = find_mut(&mut self.metadata.folders, id) else { return Err(self.missing(id)) };
781 if let Some(p) = new_parent {
782 let mut inside = false;
783 folder.walk(&mut |x| inside |= x.id == p);
784 if inside {
785 return Err(self.invalid("cannot move a folder into itself"));
786 }
787 if find_mut(&mut self.metadata.folders, p).is_none() {
788 return Err(self.missing(p));
789 }
790 }
791 let folder = remove(&mut self.metadata.folders, id).ok_or_else(|| self.missing(id))?;
792 let siblings = match new_parent {
793 None => &mut self.metadata.folders,
794 Some(p) => &mut find_mut(&mut self.metadata.folders, p).expect("parent checked above").children,
795 };
796 siblings.insert(index.min(siblings.len()), folder);
797 self.save_metadata()
798 }
799
800 pub fn sort_subfolders(&mut self, parent: Option<&str>, sort: SubfolderSort) -> Result<()> {
807 fn sort_level(list: &mut [Folder], sort: SubfolderSort) {
808 list.sort_by(|a, b| crate::search::natural_cmp(&a.name, &b.name));
809 if sort.descending {
810 list.reverse();
811 }
812 if sort.all_levels {
813 for f in list {
814 sort_level(&mut f.children, sort);
815 }
816 }
817 }
818 let list = match parent {
819 None => &mut self.metadata.folders,
820 Some(p) => match find_mut(&mut self.metadata.folders, p) {
821 Some(f) => &mut f.children,
822 None => return Err(self.missing(p)),
823 },
824 };
825 sort_level(list, sort);
826 self.save_metadata()
827 }
828
829 pub fn add_quick_access(&mut self, kind: &str, id: &str) -> Result<()> {
832 if !self.metadata.quick_access.iter().any(|q| q.get("id").and_then(|v| v.as_str()) == Some(id)) {
833 self.metadata.quick_access.push(serde_json::json!({"type": kind, "id": id}));
834 }
835 self.save_metadata()
836 }
837
838 pub fn place_quick_access(&mut self, kind: &str, id: &str, target: &str, below: bool) -> Result<()> {
844 if id == target {
845 return Ok(());
846 }
847 let key = |q: &serde_json::Value| q.get("id").and_then(|v| v.as_str()).map(String::from);
848 let entry = match self.metadata.quick_access.iter().position(|q| key(q).as_deref() == Some(id)) {
850 Some(i) => self.metadata.quick_access.remove(i),
851 None => serde_json::json!({"type": kind, "id": id}),
852 };
853 let at = self.metadata.quick_access.iter().position(|q| key(q).as_deref() == Some(target));
854 let at = at.map_or(self.metadata.quick_access.len(), |i| i + usize::from(below));
855 self.metadata.quick_access.insert(at, entry);
856 self.save_metadata()
857 }
858
859 pub fn remove_quick_access(&mut self, id: &str) -> Result<()> {
861 self.metadata.quick_access.retain(|q| q.get("id").and_then(|v| v.as_str()) != Some(id));
862 self.save_metadata()
863 }
864
865 pub fn save_metadata(&mut self) -> Result<()> {
869 self.metadata.modification_time = now_ms();
870 self.journal_metadata();
871 if self.batching(|d| d.metadata = true) {
872 return Ok(());
873 }
874 self.write_metadata_file()
875 }
876
877 pub(crate) fn save_tags(&mut self) -> Result<()> {
879 self.journal_tags();
880 if self.batching(|d| d.tags = true) {
881 return Ok(());
882 }
883 write_json(&self.root().join("tags.json"), &self.tags)
884 }
885
886 fn expect_folders(&self, ids: &[String]) -> Result<()> {
888 let known = self.folder_ids();
889 match ids.iter().find(|id| !known.contains(id.as_str())) {
890 Some(id) => Err(self.missing(id)),
891 None => Ok(()),
892 }
893 }
894
895 fn missing(&self, id: &str) -> Error {
896 Error::Import { path: self.root().to_path_buf(), reason: format!("no folder {id}") }
897 }
898
899 pub(crate) fn invalid(&self, reason: impl Into<String>) -> Error {
900 Error::Import { path: self.root().to_path_buf(), reason: reason.into() }
901 }
902
903 pub(crate) fn check_color(&self, color: Option<&str>) -> Result<()> {
905 match color {
906 Some(c) if !ICON_COLORS.contains(&c) => Err(self.invalid(format!("unknown colour {c}"))),
907 _ => Ok(()),
908 }
909 }
910}
911
912#[derive(Debug, Clone, Copy, PartialEq, Eq)]
914pub enum FolderPlace {
915 Above,
916 Into,
917 Below,
918}
919
920fn parent_map(roots: &[Folder]) -> HashMap<&str, Option<&str>> {
922 fn go<'a>(list: &'a [Folder], parent: Option<&'a str>, out: &mut HashMap<&'a str, Option<&'a str>>) {
923 for f in list {
924 out.insert(&f.id, parent);
925 go(&f.children, Some(&f.id), out);
926 }
927 }
928 let mut out = HashMap::new();
929 go(roots, None, &mut out);
930 out
931}
932
933fn folder_path<'a>(roots: &'a [Folder], id: &str) -> Vec<&'a str> {
935 let parents = parent_map(roots);
936 let mut path = Vec::new();
937 let mut cur = parents.get_key_value(id).map(|(k, _)| *k);
938 while let Some(c) = cur {
939 path.push(c);
940 cur = parents[c];
941 }
942 path
943}
944
945fn find_mut<'a>(folders: &'a mut [Folder], id: &str) -> Option<&'a mut Folder> {
946 for f in folders {
947 if f.id == id {
948 return Some(f);
949 }
950 if let Some(found) = find_mut(&mut f.children, id) {
951 return Some(found);
952 }
953 }
954 None
955}
956
957fn remove(folders: &mut Vec<Folder>, id: &str) -> Option<Folder> {
958 if let Some(pos) = folders.iter().position(|f| f.id == id) {
959 return Some(folders.remove(pos));
960 }
961 folders.iter_mut().find_map(|f| remove(&mut f.children, id))
962}
963
964pub(crate) fn now_ms() -> u64 {
965 SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_millis() as u64)
966}
967
968fn encode_password(p: &str) -> Option<String> {
970 use base64::Engine;
971 let bytes: Option<Vec<u8>> = p.chars().map(|c| u8::try_from(u32::from(c)).ok()).collect();
972 Some(base64::engine::general_purpose::STANDARD.encode(bytes?))
973}
974
975#[cfg(test)]
976mod tests {
977 use super::*;
978
979 #[test]
980 fn subfolders_sort_by_name_with_numbers_by_value() {
981 let tmp = tempfile::tempdir().unwrap();
982 let mut lib = Library::create(tmp.path().join("S.library")).unwrap();
983 let top = lib.create_folder(None, "Top").unwrap();
984 for n in ["folder 10", "Folder 2", "beta", "Alpha", "folder 1"] {
985 let id = lib.create_folder(Some(&top), n).unwrap();
986 for c in ["z", "B", "a"] {
987 lib.create_folder(Some(&id), c).unwrap();
988 }
989 }
990 lib.create_folder(None, "Another").unwrap();
991 let names = |l: &[Folder]| l.iter().map(|f| f.name.clone()).collect::<Vec<_>>();
992 let stamp = lib.metadata.folders[0].children[0].modification_time;
993
994 lib.sort_subfolders(Some(&top), SubfolderSort::default()).unwrap();
995 let reopened = Library::open(lib.root()).unwrap();
996 let kids = &reopened.metadata.folders[0].children;
997 assert_eq!(names(kids), ["Alpha", "beta", "folder 1", "Folder 2", "folder 10"]);
998 assert_eq!(names(&kids[0].children), ["z", "B", "a"], "one level only");
999 assert_eq!(names(&reopened.metadata.folders), ["Top", "Another"], "other levels untouched");
1000 assert!(kids.iter().any(|f| f.modification_time == stamp), "folders keep their times");
1001
1002 lib.sort_subfolders(None, SubfolderSort { descending: true, all_levels: true }).unwrap();
1003 assert_eq!(names(&lib.metadata.folders), ["Top", "Another"]);
1004 let kids = &lib.metadata.folders[0].children;
1005 assert_eq!(names(kids), ["folder 10", "Folder 2", "folder 1", "beta", "Alpha"]);
1006 assert_eq!(names(&kids[0].children), ["z", "B", "a"]);
1007 lib.sort_subfolders(None, SubfolderSort { descending: false, all_levels: true }).unwrap();
1008 assert_eq!(names(&lib.metadata.folders[1].children[0].children), ["a", "B", "z"]);
1009 assert!(lib.sort_subfolders(Some("NOPE"), SubfolderSort::default()).is_err());
1010 }
1011
1012 #[test]
1013 fn tag_group_descriptions_and_drag_order() {
1014 let tmp = tempfile::tempdir().unwrap();
1015 let mut lib = Library::create(tmp.path().join("G.library")).unwrap();
1016 let a = lib.create_tag_group("A", &["x".into()], Some("red")).unwrap();
1017 let b = lib.create_tag_group("B", &[], None).unwrap();
1018 let c = lib.create_tag_group("C", &[], None).unwrap();
1019 let path = lib.root().join("metadata.json");
1020 let text = || std::fs::read_to_string(&path).unwrap();
1021 assert!(!text().contains("description"), "no key until one is set");
1022 lib.set_tag_group_description(&a, "Warm & bright").unwrap();
1023 assert!(text().contains(&format!(r#"{{"id":"{a}","name":"A","tags":["x"],"color":"red","description":"Warm & bright"}}"#)), "{}", text());
1024 lib.set_tag_group_description(&a, "").unwrap();
1025 assert!(text().contains(r#""color":"red","description":""}"#), "cleared, the key stays: {}", text());
1026 assert!(lib.set_tag_group_description("NOPE", "x").is_err());
1027
1028 lib.reorder_tag_group(&c, 0).unwrap();
1030 lib.reorder_tag_group(&a, 2).unwrap();
1031 let order: Vec<&str> = lib.metadata.tags_groups.iter().map(|g| g.id.as_str()).collect();
1032 assert_eq!(order, [c.as_str(), b.as_str(), a.as_str()]);
1033 let reopened = Library::open(lib.root()).unwrap();
1034 assert_eq!(reopened.metadata.tags_groups[2].description.as_deref(), Some(""));
1035 }
1036
1037 fn with_items(dir: &std::path::Path) -> (Library, Vec<String>) {
1039 let mut lib = Library::create(dir.join("B.library")).unwrap();
1040 let mut ids = Vec::new();
1041 for n in 0..3 {
1042 let path = dir.join(format!("{n}.png"));
1043 image::RgbImage::from_pixel(4, 4, image::Rgb([n * 80, 20, 20])).save(&path).unwrap();
1044 ids.push(lib.import_file(&path, &crate::import::ImportOptions::default()).unwrap());
1045 }
1046 (lib, ids)
1047 }
1048
1049 fn file_into(lib: &mut Library, item: &str, folders: &[&String]) {
1050 let mut i = lib.item(item).unwrap().clone();
1051 i.folders = folders.iter().map(|f| f.to_string()).collect();
1052 lib.update_item(i).unwrap();
1053 }
1054
1055 fn names(l: &[Folder]) -> Vec<String> {
1056 l.iter().map(|f| f.name.clone()).collect()
1057 }
1058
1059 #[test]
1060 fn deleting_several_folders_trashes_or_keeps_items() {
1061 let tmp = tempfile::tempdir().unwrap();
1062 let (mut lib, items) = with_items(tmp.path());
1063 let a = lib.create_folder(None, "A").unwrap();
1064 let a1 = lib.create_folder(Some(&a), "A1").unwrap();
1065 let b = lib.create_folder(None, "B").unwrap();
1066 let c = lib.create_folder(None, "C").unwrap();
1067 lib.add_quick_access("folder", &a1).unwrap();
1068 lib.add_quick_access("folder", &c).unwrap();
1069 file_into(&mut lib, &items[0], &[&a1]);
1070 file_into(&mut lib, &items[1], &[&a, &b]);
1071 file_into(&mut lib, &items[2], &[&b, &c]);
1072
1073 lib.delete_folders(&[a.clone(), b.clone()], false).unwrap();
1075 let kept = Library::open(lib.root()).unwrap();
1076 assert_eq!(names(&kept.metadata.folders), ["C"]);
1077 assert!(kept.item(&items[0]).unwrap().folders.is_empty());
1078 assert!(kept.items().iter().all(|i| !i.is_deleted));
1079 assert_eq!(kept.item(&items[2]).unwrap().folders, std::slice::from_ref(&c));
1080 assert_eq!(kept.metadata.quick_access.len(), 1, "the subfolder is unpinned");
1081
1082 let tmp = tempfile::tempdir().unwrap();
1084 let (mut lib, items) = with_items(tmp.path());
1085 let a = lib.create_folder(None, "A").unwrap();
1086 let a1 = lib.create_folder(Some(&a), "A1").unwrap();
1087 let b = lib.create_folder(None, "B").unwrap();
1088 let c = lib.create_folder(None, "C").unwrap();
1089 file_into(&mut lib, &items[0], &[&a1]);
1090 file_into(&mut lib, &items[1], &[&a, &b]);
1091 file_into(&mut lib, &items[2], &[&b, &c]);
1092 lib.delete_folders(&[a1.clone(), a.clone(), b.clone()], true).unwrap();
1094 let got = Library::open(lib.root()).unwrap();
1095 assert!(got.item(&items[0]).unwrap().is_deleted);
1096 assert!(got.item(&items[1]).unwrap().is_deleted);
1097 assert!(!got.item(&items[2]).unwrap().is_deleted);
1098 assert!(got.item(&items[0]).unwrap().extra.get("deletedTime").is_none(), "as Eagle, no deletedTime");
1099
1100 let before = lib.metadata.folders.clone();
1101 assert!(lib.delete_folders(&[c.clone(), "NOPE".into()], true).is_err());
1102 assert_eq!(lib.metadata.folders, before, "nothing changes on an unknown id");
1103 }
1104
1105 #[test]
1106 fn batch_rename_restyle_and_sort() {
1107 let tmp = tempfile::tempdir().unwrap();
1108 let mut lib = Library::create(tmp.path().join("R.library")).unwrap();
1109 let a = lib.create_folder(None, "A").unwrap();
1110 let b = lib.create_folder(Some(&a), "B").unwrap();
1111 let c = lib.create_folder(None, "C").unwrap();
1112 let stamp = lib.metadata.folders[0].modification_time;
1113 lib.set_folder_order(&c, Some(FolderOrder::By(crate::search::OrderBy::Name)), false).unwrap();
1114
1115 lib.rename_folders(&[(a.clone(), "Cafe\u{301} 1".into()), (b.clone(), "Two".into())]).unwrap();
1116 let got = Library::open(lib.root()).unwrap();
1117 assert_eq!(got.metadata.folders[0].name, "Caf\u{e9} 1", "NFC as Eagle saves");
1118 assert_eq!(got.metadata.folders[0].children[0].name, "Two");
1119 assert_eq!(got.metadata.folders[0].modification_time, stamp);
1120 assert!(lib.rename_folders(&[(c.clone(), "X".into()), ("NOPE".into(), "Y".into())]).is_err());
1121 assert_eq!(lib.metadata.folders[1].name, "C");
1122
1123 let all = [a.clone(), b.clone(), c.clone()];
1124 lib.restyle_folders(&all, None, Some(Some("red"))).unwrap();
1125 lib.restyle_folders(&all[..2], Some(Some("star")), None).unwrap();
1126 let got = Library::open(lib.root()).unwrap();
1127 let f = got.all_folders();
1128 assert_eq!((f[0].icon.as_deref(), f[0].icon_color.as_deref()), (Some("star"), Some("red")));
1129 assert_eq!((f[2].icon.as_deref(), f[2].icon_color.as_deref()), (None, Some("red")));
1130 assert!(lib.restyle_folders(&all, None, Some(Some("mauve"))).is_err());
1131 lib.restyle_folders(&all, Some(None), Some(None)).unwrap();
1132 let text = std::fs::read_to_string(lib.root().join("metadata.json")).unwrap();
1133 assert!(!text.contains("iconColor") && !text.contains("\"icon\""), "keys removed: {text}");
1134
1135 lib.set_folders_order(&all, Some(FolderOrder::By(crate::search::OrderBy::FileSize))).unwrap();
1137 let f = lib.all_folders();
1138 assert_eq!(f.iter().map(|f| f.sort_increase).collect::<Vec<_>>(), [Some(true), Some(true), Some(false)]);
1139 assert!(f.iter().all(|f| f.order_by.as_deref() == Some("FILESIZE")), "{:?}", f.iter().map(|f| &f.order_by).collect::<Vec<_>>());
1140 lib.set_folders_increase(&all, false).unwrap();
1141 assert!(lib.all_folders().iter().all(|f| f.sort_increase == Some(false)));
1142 lib.set_folders_order(&all, None).unwrap();
1143 let text = std::fs::read_to_string(lib.root().join("metadata.json")).unwrap();
1144 assert!(!text.contains("orderBy") && !text.contains("sortIncrease"));
1145 }
1146
1147 #[test]
1148 fn moving_several_folders() {
1149 let tmp = tempfile::tempdir().unwrap();
1150 let mut lib = Library::create(tmp.path().join("M.library")).unwrap();
1151 let a = lib.create_folder(None, "A").unwrap();
1152 let a1 = lib.create_folder(Some(&a), "A1").unwrap();
1153 let a2 = lib.create_folder(Some(&a), "A2").unwrap();
1154 let b = lib.create_folder(None, "B").unwrap();
1155 let c = lib.create_folder(None, "C").unwrap();
1156 let d = lib.create_folder(None, "D").unwrap();
1157 let tree = |l: &Library| -> Vec<String> {
1158 fn go(list: &[Folder], depth: usize, out: &mut Vec<String>) {
1159 for f in list {
1160 out.push(format!("{}{}", " ".repeat(depth), f.name));
1161 go(&f.children, depth + 1, out);
1162 }
1163 }
1164 let mut out = Vec::new();
1165 go(&l.metadata.folders, 0, &mut out);
1166 out
1167 };
1168
1169 let start = tree(&lib);
1171 lib.begin_transaction("move");
1172 lib.move_folders(&[c.clone(), d.clone()], &a, FolderPlace::Into).unwrap();
1173 let t = lib.end_transaction().unwrap();
1174 lib.revert(t).unwrap();
1175 assert_eq!(tree(&Library::open(lib.root()).unwrap()), start);
1176
1177 lib.move_folders(&[c.clone(), a2.clone()], &b, FolderPlace::Above).unwrap();
1179 assert_eq!(tree(&lib), ["A", " A1", "A2", "C", "B", "D"]);
1180 lib.move_folders(&[a2.clone(), d.clone()], &a1, FolderPlace::Below).unwrap();
1181 assert_eq!(tree(&lib), ["A", " A1", " A2", " D", "C", "B"]);
1182 lib.move_folders(&[a1.clone(), a.clone()], &b, FolderPlace::Into).unwrap();
1185 assert_eq!(tree(&Library::open(lib.root()).unwrap()), ["C", "B", " A", " A1", " A2", " D"]);
1186
1187 let before = tree(&lib);
1188 assert!(lib.move_folders(std::slice::from_ref(&b), &a2, FolderPlace::Into).is_err(), "into its own descendant");
1189 assert!(lib.move_folders(&[b.clone(), c.clone()], &c, FolderPlace::Above).is_err(), "onto a moved folder");
1190 assert!(lib.move_folders(std::slice::from_ref(&c), "NOPE", FolderPlace::Above).is_err());
1191 assert_eq!(tree(&lib), before);
1192
1193 lib.move_folders_to_top(&[a1.clone(), a.clone(), c.clone()]).unwrap();
1195 assert_eq!(tree(&Library::open(lib.root()).unwrap()), ["B", "C", "A", " A1", " A2", " D"]);
1196 let before = tree(&lib);
1197 assert!(lib.move_folders_to_top(&["nope".to_string()]).is_err());
1198 assert_eq!(tree(&lib), before);
1199 }
1200
1201 #[test]
1202 fn duplicating_a_folder() {
1203 let tmp = tempfile::tempdir().unwrap();
1204 let (mut lib, items) = with_items(tmp.path());
1205 let a = lib.create_folder(None, "A").unwrap();
1206 let a1 = lib.create_folder(Some(&a), "A1").unwrap();
1207 lib.create_folder(Some(&a1), "A1x").unwrap();
1208 let b = lib.create_folder(None, "B").unwrap();
1209 lib.set_folder_icon(&a1, Some("star"), Some("blue")).unwrap();
1210 lib.set_folder_password(&a1, Some(("pw", "hint"))).unwrap();
1211 lib.set_folder_description(&a1, "about").unwrap();
1212 lib.set_folder_cover(&a1, Some(&items[0])).unwrap();
1213 lib.set_folder_order(&a1, Some(FolderOrder::Random), false).unwrap();
1214 file_into(&mut lib, &items[0], &[&a1]);
1215 let original = lib.metadata.folders[0].children[0].clone();
1216
1217 let copy = lib.duplicate_folder(&a1).unwrap();
1218 let got = Library::open(lib.root()).unwrap();
1219 let kids = &got.metadata.folders[0].children;
1220 assert_eq!(names(kids), ["A1", "A1"], "right after the original, same name");
1221 assert_eq!(kids[0], original, "the original is untouched");
1222 let dup = &kids[1];
1223 assert_eq!(dup.id, copy);
1224 assert_ne!(dup.children[0].id, original.children[0].id);
1225 assert_eq!(dup.children[0].name, "A1x");
1226 assert_eq!((dup.icon.as_deref(), dup.icon_color.as_deref(), dup.description.as_str()), (Some("star"), Some("blue"), "about"));
1227 assert_eq!((dup.cover_id.as_deref(), dup.order_by.as_deref(), dup.sort_increase), (Some(items[0].as_str()), Some("RANDOM"), Some(false)));
1228 assert!(!dup.is_locked() && dup.password_tips.is_empty());
1229 assert!(dup.modification_time >= original.modification_time);
1230 assert_eq!(got.item(&items[0]).unwrap().folders, std::slice::from_ref(&a1), "items are not copied");
1231
1232 lib.duplicate_folder(&b).unwrap();
1234 assert_eq!(names(&lib.metadata.folders), ["A", "B", "B"]);
1235 assert!(lib.duplicate_folder("NOPE").is_err());
1236 }
1237
1238 #[test]
1239 fn passwords_encode_like_btoa() {
1240 assert_eq!(encode_password("123").as_deref(), Some("MTIz"));
1241 assert_eq!(encode_password("é").as_deref(), Some("6Q=="));
1243 assert_eq!(encode_password("密码"), None);
1244 }
1245}