1use std::collections::BTreeSet;
16
17use crate::error::Result;
18use crate::library::{Library, write_json};
19use crate::model::{Item, LibraryMetadata, SavedFilter, Tags};
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct Transaction {
24 pub label: String,
26 items: Vec<Item>,
28 created: Vec<String>,
30 metadata: Option<LibraryMetadata>,
31 tags: Option<Tags>,
32 saved_filters: Option<Vec<SavedFilter>>,
33 pub irreversible: bool,
37}
38
39impl Transaction {
40 fn new(label: String) -> Transaction {
41 Transaction { label, items: Vec::new(), created: Vec::new(), metadata: None, tags: None, saved_filters: None, irreversible: false }
42 }
43
44 pub fn is_empty(&self) -> bool {
46 self.items.is_empty() && self.created.is_empty() && self.metadata.is_none() && self.tags.is_none() && self.saved_filters.is_none()
47 }
48
49 pub fn item_ids(&self) -> impl Iterator<Item = &str> {
51 self.items.iter().map(|i| i.id.as_str()).chain(self.created.iter().map(String::as_str))
52 }
53
54 pub fn old_item(&self, id: &str) -> Option<&Item> {
56 self.items.iter().find(|i| i.id == id)
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
62pub enum WriteMode {
63 #[default]
66 Immediate,
67 Batched,
69}
70
71#[derive(Debug, Clone, Default, PartialEq)]
73pub struct FlushReport {
74 pub items: usize,
76 pub mtime: bool,
77 pub metadata: bool,
78 pub tags: bool,
79 pub saved_filters: bool,
80}
81
82impl FlushReport {
83 pub fn wants_backup(&self) -> bool {
87 self.mtime || self.metadata
88 }
89}
90
91#[derive(Debug, Clone, Default)]
93pub(crate) struct Dirty {
94 pub(crate) items: BTreeSet<String>,
95 pub(crate) mtime: bool,
96 pub(crate) metadata: bool,
97 pub(crate) tags: bool,
98 pub(crate) saved_filters: bool,
99}
100
101impl Dirty {
102 fn any(&self) -> bool {
103 !self.items.is_empty() || self.mtime || self.metadata || self.tags || self.saved_filters
104 }
105}
106
107#[derive(Debug, Clone)]
109pub(crate) struct Committed {
110 pub(crate) metadata: LibraryMetadata,
111 pub(crate) tags: Tags,
112 pub(crate) saved_filters: Vec<SavedFilter>,
113}
114
115impl Library {
116 pub fn begin_transaction(&mut self, label: impl Into<String>) {
120 self.journal = Some(Transaction::new(label.into()));
121 }
122
123 pub fn set_transaction_label(&mut self, label: impl Into<String>) {
126 if let Some(j) = &mut self.journal {
127 j.label = label.into();
128 }
129 }
130
131 pub fn end_transaction(&mut self) -> Option<Transaction> {
134 self.journal.take().filter(|t| !t.is_empty() || t.irreversible)
135 }
136
137 pub fn revert(&mut self, tx: Transaction) -> Result<Transaction> {
147 let outer = self.journal.take();
148 self.begin_transaction(tx.label.clone());
149 let result = self.apply(&tx);
150 let inverse = self.journal.take().unwrap_or_else(|| Transaction::new(tx.label.clone()));
151 self.journal = outer;
152 result.map(|()| inverse)
153 }
154
155 fn apply(&mut self, tx: &Transaction) -> Result<()> {
156 if let Some(m) = &tx.metadata {
157 self.metadata = m.clone();
158 self.save_metadata()?;
159 }
160 if let Some(t) = &tx.tags {
161 self.tags = t.clone();
162 self.save_tags()?;
163 }
164 if let Some(f) = &tx.saved_filters {
165 self.saved_filters = f.clone();
166 self.save_filters()?;
167 }
168 for old in &tx.items {
169 if self.item(&old.id).is_some() {
170 self.update_item(old.clone())?;
171 }
172 }
173 let created: Vec<String> = tx.created.iter().filter(|id| self.item(id).is_some_and(|i| !i.is_deleted)).cloned().collect();
174 self.set_trashed(&created, true)
175 }
176
177 pub(crate) fn journal_item(&mut self, id: &str) {
180 let Some(j) = &self.journal else { return };
181 if j.items.iter().any(|i| i.id == id) || j.created.iter().any(|c| c == id) {
182 return;
183 }
184 let old = self.item(id).cloned();
185 let j = self.journal.as_mut().expect("checked above");
186 match old {
187 Some(item) => j.items.push(item),
188 None => j.created.push(id.to_string()),
189 }
190 }
191
192 pub(crate) fn journal_metadata(&mut self) {
195 let prev = std::mem::replace(&mut self.committed.metadata, self.metadata.clone());
196 if let Some(j) = &mut self.journal {
197 j.metadata.get_or_insert(prev);
198 }
199 }
200
201 pub(crate) fn journal_tags(&mut self) {
202 let prev = std::mem::replace(&mut self.committed.tags, self.tags.clone());
203 if let Some(j) = &mut self.journal {
204 j.tags.get_or_insert(prev);
205 }
206 }
207
208 pub(crate) fn journal_saved_filters(&mut self) {
209 let prev = std::mem::replace(&mut self.committed.saved_filters, self.saved_filters.clone());
210 if let Some(j) = &mut self.journal {
211 j.saved_filters.get_or_insert(prev);
212 }
213 }
214
215 pub(crate) fn batching(&mut self, mark: impl FnOnce(&mut Dirty)) -> bool {
218 match self.write_mode {
219 WriteMode::Immediate => false,
220 WriteMode::Batched => {
221 mark(&mut self.dirty);
222 true
223 }
224 }
225 }
226
227 pub fn write_mode(&self) -> WriteMode {
229 self.write_mode
230 }
231
232 pub fn set_write_mode(&mut self, mode: WriteMode) -> Result<()> {
235 if mode == WriteMode::Immediate && self.write_mode == WriteMode::Batched {
236 self.flush()?;
237 }
238 self.write_mode = mode;
239 Ok(())
240 }
241
242 pub fn has_pending_writes(&self) -> bool {
244 self.dirty.any()
245 }
246
247 pub fn flush(&mut self) -> Result<FlushReport> {
253 let mut report = FlushReport::default();
254 if !self.dirty.any() {
255 return Ok(report);
256 }
257 let root = self.root().to_path_buf();
258 for id in &self.dirty.items {
259 if let Some(item) = self.item(id) {
261 write_json(&item.info_dir(&root).join("metadata.json"), item)?;
262 report.items += 1;
263 }
264 }
265 if self.dirty.mtime {
266 write_json(&root.join("mtime.json"), &self.mtime)?;
267 report.mtime = true;
268 }
269 if self.dirty.metadata {
270 if let Err(e) = self.write_metadata_file() {
271 if matches!(e, crate::Error::SyncConflict(_) | crate::Error::NewerLibrary { .. }) {
274 self.dirty.metadata = false;
275 }
276 return Err(e);
277 }
278 report.metadata = true;
279 }
280 if self.dirty.tags {
281 write_json(&root.join("tags.json"), &self.tags)?;
282 report.tags = true;
283 }
284 if self.dirty.saved_filters {
285 write_json(&root.join("saved-filters.json"), &self.saved_filters)?;
286 report.saved_filters = true;
287 }
288 self.dirty = Dirty::default();
289 Ok(report)
290 }
291}
292
293impl Drop for Library {
299 fn drop(&mut self) {
300 if self.write_mode == WriteMode::Batched && self.dirty.any() {
301 let _ = self.flush();
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::import::ImportOptions;
310 use crate::smart;
311 use std::collections::BTreeMap;
312 use std::fs;
313 use std::path::{Path, PathBuf};
314
315 fn setup(dir: &Path) -> (Library, String, String) {
317 let mut lib = Library::create(dir.join("Test.library")).unwrap();
318 let mut ids = Vec::new();
319 for (name, rgb) in [("a", [200, 30, 30]), ("b", [30, 30, 200])] {
320 let path = dir.join(format!("{name}.png"));
321 image::RgbImage::from_pixel(8, 6, image::Rgb(rgb)).save(&path).unwrap();
322 ids.push(lib.import_file(&path, &ImportOptions::default()).unwrap());
323 }
324 let b = ids.pop().unwrap();
325 (lib, ids.pop().unwrap(), b)
326 }
327
328 fn tx(lib: &mut Library, edit: impl FnOnce(&mut Library)) -> Transaction {
330 lib.begin_transaction("edit");
331 edit(lib);
332 lib.end_transaction().expect("something changed")
333 }
334
335 fn item(lib: &Library, id: &str) -> Item {
336 lib.item(id).unwrap().clone()
337 }
338
339 fn content(lib: &Library, id: &str) -> Item {
341 let mut i = item(lib, id);
342 i.last_modified = 0;
343 i
344 }
345
346 fn snapshot(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
348 let mut out = BTreeMap::new();
349 let mut stack = vec![root.to_path_buf()];
350 while let Some(d) = stack.pop() {
351 for e in fs::read_dir(&d).unwrap().flatten() {
352 let p = e.path();
353 if p.is_dir() {
354 if p.file_name().is_some_and(|n| n != "backup") {
355 stack.push(p);
356 }
357 } else {
358 out.insert(p.strip_prefix(root).unwrap().to_path_buf(), fs::read(&p).unwrap());
359 }
360 }
361 }
362 out
363 }
364
365 #[test]
366 fn rename_undo_and_redo_move_the_files() {
367 let tmp = tempfile::tempdir().unwrap();
368 let (mut lib, a, _) = setup(tmp.path());
369 let before = content(&lib, &a);
370 let t = tx(&mut lib, |lib| {
371 let mut i = item(lib, &a);
372 i.name = "renamed".into();
373 lib.update_item(i).unwrap();
374 });
375 let root = lib.root().to_path_buf();
376 assert!(root.join(format!("images/{a}.info/renamed.png")).is_file());
377 let redo = lib.revert(t).unwrap();
378 assert_eq!(content(&lib, &a), before);
379 assert!(root.join(format!("images/{a}.info/a.png")).is_file());
380 assert!(!root.join(format!("images/{a}.info/renamed_thumbnail.png")).exists());
381 assert!(!root.join(format!("images/{a}.info/renamed.png")).exists());
382 let on_disk = Library::open(&root).unwrap();
384 assert_eq!(on_disk.item(&a), lib.item(&a));
385 let undo = lib.revert(redo).unwrap();
386 assert_eq!(item(&lib, &a).name, "renamed");
387 assert!(root.join(format!("images/{a}.info/renamed.png")).is_file());
388 lib.revert(undo).unwrap();
389 assert_eq!(item(&lib, &a).name, "a");
390 }
391
392 #[test]
393 fn trash_and_restore_undo() {
394 let tmp = tempfile::tempdir().unwrap();
395 let (mut lib, a, b) = setup(tmp.path());
396 let before = (content(&lib, &a), content(&lib, &b));
397 let t = tx(&mut lib, |lib| lib.set_trashed(&[a.clone(), b.clone()], true).unwrap());
398 assert!(item(&lib, &a).is_deleted);
399 let redo = lib.revert(t).unwrap();
400 assert_eq!((content(&lib, &a), content(&lib, &b)), before);
401 assert!(!item(&lib, &a).extra.contains_key("deletedTime"));
402 lib.revert(redo).unwrap();
403 assert!(item(&lib, &a).is_deleted && item(&lib, &b).is_deleted);
404 }
405
406 #[test]
407 fn tag_edits_undo_items_and_tags_json() {
408 let tmp = tempfile::tempdir().unwrap();
409 let (mut lib, a, _) = setup(tmp.path());
410 let tags_before = lib.tags.clone();
411 let t = tx(&mut lib, |lib| {
412 let mut i = item(lib, &a);
413 i.tags.push("red".into());
414 lib.update_item(i).unwrap();
415 lib.record_tag_use(&["red".into()]).unwrap();
416 lib.set_tag_starred("red", true).unwrap();
417 });
418 let redo = lib.revert(t).unwrap();
419 assert!(item(&lib, &a).tags.is_empty());
420 assert_eq!(lib.tags, tags_before);
421 let on_disk = Library::open(lib.root()).unwrap();
422 assert_eq!(on_disk.tags, tags_before);
423 lib.revert(redo).unwrap();
424 assert_eq!(item(&lib, &a).tags, ["red"]);
425 assert_eq!(lib.tags.starred_tags, ["red"]);
426 }
427
428 #[test]
429 fn folder_edits_undo() {
430 let tmp = tempfile::tempdir().unwrap();
431 let (mut lib, a, b) = setup(tmp.path());
432 let empty = lib.metadata.folders.clone();
433 let mut f = String::new();
434 let t = tx(&mut lib, |lib| f = lib.create_folder(None, "One").unwrap());
435 lib.revert(t).unwrap();
436 assert_eq!(lib.metadata.folders, empty);
437
438 let f = lib.create_folder(None, "One").unwrap();
439 let g = lib.create_folder(None, "Two").unwrap();
440 let t = tx(&mut lib, |lib| lib.rename_folder(&f, "Uno").unwrap());
441 lib.revert(t).unwrap();
442 assert_eq!(lib.all_folders()[0].name, "One");
443
444 let tree = lib.metadata.folders.clone();
445 let t = tx(&mut lib, |lib| lib.move_folder(&g, Some(&f)).unwrap());
446 assert_eq!(lib.metadata.folders.len(), 1);
447 lib.revert(t).unwrap();
448 assert_eq!(lib.metadata.folders, tree);
449
450 for (id, folders) in [(&a, vec![f.clone()]), (&b, vec![f.clone(), g.clone()])] {
452 let mut i = item(&lib, id);
453 i.folders = folders;
454 lib.update_item(i).unwrap();
455 }
456 let before = (content(&lib, &a), content(&lib, &b));
457 let t = tx(&mut lib, |lib| lib.delete_folder(&f).unwrap());
458 assert!(item(&lib, &a).is_deleted);
459 assert_eq!(item(&lib, &b).folders, std::slice::from_ref(&g));
460 lib.revert(t).unwrap();
461 assert_eq!((content(&lib, &a), content(&lib, &b)), before);
462 assert_eq!(lib.metadata.folders, tree);
463 let on_disk = Library::open(lib.root()).unwrap();
464 assert_eq!(on_disk.metadata.folders, tree);
465 }
466
467 #[test]
472 fn folder_name_and_description_round_trip() {
473 let tmp = tempfile::tempdir().unwrap();
474 let (mut lib, _, _) = setup(tmp.path());
475 let f = lib.create_folder(None, "One").unwrap();
476 let meta = lib.root().join("metadata.json");
477 let read = || {
479 let mut v: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&meta).unwrap()).unwrap();
480 v.as_object_mut().unwrap().shift_remove("modificationTime").expect("save time");
481 v.to_string()
482 };
483 let before = read();
484 assert_eq!(before.matches(r#""name":"One","description":"""#).count(), 1, "{before}");
485 let t = tx(&mut lib, |lib| {
486 lib.rename_folder(&f, "Uno").unwrap();
487 lib.set_folder_description(&f, "Line one\nline \"two\"").unwrap();
488 });
489 let after = read();
490 assert_eq!(after, before.replace(r#""name":"One","description":"""#, r#""name":"Uno","description":"Line one\nline \"two\"""#));
491 let reopened = Library::open(lib.root()).unwrap();
492 let folder = &reopened.all_folders()[0];
493 assert_eq!((folder.name.as_str(), folder.description.as_str()), ("Uno", "Line one\nline \"two\""));
494 let redo = lib.revert(t).unwrap();
495 assert_eq!(read(), before);
496 lib.revert(redo).unwrap();
497 assert_eq!(read(), after);
498 assert!(lib.set_folder_description("missing", "x").is_err());
499 }
500
501 #[test]
502 fn tag_group_edits_undo() {
503 let tmp = tempfile::tempdir().unwrap();
504 let (mut lib, _, _) = setup(tmp.path());
505 let t = tx(&mut lib, |lib| {
506 lib.create_tag_group("Colours", &["red".into()], None).unwrap();
507 });
508 lib.revert(t).unwrap();
509 assert!(lib.metadata.tags_groups.is_empty());
510 let id = lib.create_tag_group("Colours", &["red".into()], None).unwrap();
511 let before = lib.metadata.tags_groups.clone();
512 let t = tx(&mut lib, |lib| {
513 lib.rename_tag_group(&id, "Hues").unwrap();
514 lib.add_tags_to_group(&id, &["blue".into()]).unwrap();
515 });
516 let redo = lib.revert(t).unwrap();
517 assert_eq!(lib.metadata.tags_groups, before);
518 lib.revert(redo).unwrap();
519 assert_eq!(lib.metadata.tags_groups[0].name, "Hues");
520 assert_eq!(lib.metadata.tags_groups[0].tags, ["red", "blue"]);
521 }
522
523 #[test]
524 fn smart_folder_and_saved_filter_edits_undo() {
525 let tmp = tempfile::tempdir().unwrap();
526 let (mut lib, _, _) = setup(tmp.path());
527 let rule = serde_json::json!({"conditions": [{"match": "AND", "rules": [{"property": "name", "method": "contain", "value": "a"}]}]});
528 let conditions = rule["conditions"].as_array().unwrap().clone();
529 let t = tx(&mut lib, |lib| {
530 lib.create_smart_folder("Smart", conditions.clone()).unwrap();
531 });
532 lib.revert(t).unwrap();
533 assert!(lib.smart_folders().is_empty());
534 let id = lib.create_smart_folder("Smart", conditions.clone()).unwrap();
535 let t = tx(&mut lib, |lib| lib.update_smart_folder(&id, "Clever", Vec::new()).unwrap());
536 lib.revert(t).unwrap();
537 let sf: Vec<smart::SmartFolder> = lib.smart_folders();
538 assert_eq!(sf[0].name, "Smart");
539 let t = tx(&mut lib, |lib| lib.delete_smart_folder(&id).unwrap());
540 lib.revert(t).unwrap();
541 assert_eq!(lib.smart_folders().len(), 1);
542
543 let t = tx(&mut lib, |lib| {
544 lib.create_saved_filter("Saved", rule.clone()).unwrap();
545 });
546 lib.revert(t).unwrap();
547 assert!(lib.saved_filters.is_empty());
548 assert_eq!(fs::read(lib.root().join("saved-filters.json")).unwrap(), b"[]");
549 }
550
551 #[test]
552 fn imports_undo_to_the_trash() {
553 let tmp = tempfile::tempdir().unwrap();
554 let (mut lib, _, _) = setup(tmp.path());
555 let path = tmp.path().join("c.png");
556 image::RgbImage::from_pixel(4, 4, image::Rgb([0, 200, 0])).save(&path).unwrap();
557 let mut c = String::new();
558 let t = tx(&mut lib, |lib| c = lib.import_file(&path, &ImportOptions::default()).unwrap());
559 assert_eq!(t.item_ids().collect::<Vec<_>>(), [c.as_str()]);
560 let redo = lib.revert(t).unwrap();
561 assert!(item(&lib, &c).is_deleted);
562 assert!(item(&lib, &c).original_path(lib.root()).is_file());
563 lib.revert(redo).unwrap();
564 assert!(!item(&lib, &c).is_deleted);
565 }
566
567 #[test]
568 fn permanent_deletes_are_irreversible() {
569 let tmp = tempfile::tempdir().unwrap();
570 let (mut lib, a, _) = setup(tmp.path());
571 lib.begin_transaction("Delete");
572 lib.delete_permanently(&[a]).unwrap();
573 assert!(lib.end_transaction().unwrap().irreversible);
574 lib.begin_transaction("Nothing");
575 assert_eq!(lib.end_transaction(), None);
576 }
577
578 #[test]
579 fn revert_inside_an_open_transaction_is_not_recorded_in_it() {
580 let tmp = tempfile::tempdir().unwrap();
581 let (mut lib, a, _) = setup(tmp.path());
582 let t = tx(&mut lib, |lib| lib.set_trashed(std::slice::from_ref(&a), true).unwrap());
583 lib.begin_transaction("frame");
584 lib.revert(t).unwrap();
585 assert_eq!(lib.end_transaction(), None);
586 }
587
588 fn edits(lib: &mut Library, a: &str, b: &str) {
590 let f = lib.create_folder(None, "F").unwrap();
591 lib.rename_folder(&f, "G").unwrap();
592 for id in [a, b] {
593 let mut i = item(lib, id);
594 i.tags.push("x".into());
595 i.folders.push(f.clone());
596 lib.update_item(i).unwrap();
597 }
598 let mut i = item(lib, a);
599 i.name = "first".into();
600 lib.update_item(i).unwrap();
601 lib.record_tag_use(&["x".into()]).unwrap();
602 lib.create_tag_group("G", &["x".into()], None).unwrap();
603 lib.create_saved_filter("S", serde_json::json!({})).unwrap();
604 lib.set_trashed(&[b.to_string()], true).unwrap();
605 }
606
607 fn normalise(lib: &mut Library) {
609 lib.metadata.modification_time = 0;
610 for i in lib.items_mut() {
611 i.last_modified = 0;
612 i.extra.remove("deletedTime");
613 }
614 for e in &mut lib.mtime.entries {
615 e.1 = 0;
616 }
617 }
618
619 #[test]
620 fn batched_flush_writes_what_immediate_writes() {
621 let tmp = tempfile::tempdir().unwrap();
622 let (one, two) = (tmp.path().join("one"), tmp.path().join("two"));
623 fs::create_dir_all(&one).unwrap();
624 fs::create_dir_all(&two).unwrap();
625 let (mut imm, a, b) = setup(&one);
626 let (mut bat, a2, b2) = setup(&two);
627 edits(&mut imm, &a, &b);
628
629 bat.set_write_mode(WriteMode::Batched).unwrap();
630 let before = snapshot(bat.root());
631 edits(&mut bat, &a2, &b2);
632 assert!(bat.has_pending_writes());
633 let mid = snapshot(bat.root());
635 let json = |s: &BTreeMap<PathBuf, Vec<u8>>| s.iter().filter(|(p, _)| p.extension().is_some_and(|e| e == "json")).map(|(p, v)| (p.clone(), v.clone())).collect::<BTreeMap<_, _>>();
636 assert_eq!(json(&mid), json(&before));
637 let report = bat.flush().unwrap();
638 assert_eq!(report.items, 2);
639 assert!(report.mtime && report.metadata && report.tags && report.saved_filters);
640 assert!(report.wants_backup());
641 assert!(bat.backups().unwrap().is_empty());
643 assert!(!bat.has_pending_writes());
644 assert_eq!(bat.flush().unwrap(), FlushReport::default());
645
646 for lib in [&imm, &bat] {
649 let root = lib.root();
650 assert_eq!(fs::read(root.join("metadata.json")).unwrap(), serde_json::to_vec(&lib.metadata).unwrap());
651 assert_eq!(fs::read(root.join("mtime.json")).unwrap(), serde_json::to_vec(&lib.mtime).unwrap());
652 assert_eq!(fs::read(root.join("tags.json")).unwrap(), serde_json::to_vec(&lib.tags).unwrap());
653 assert_eq!(fs::read(root.join("saved-filters.json")).unwrap(), serde_json::to_vec(&lib.saved_filters).unwrap());
654 for i in lib.items() {
655 assert_eq!(fs::read(i.info_dir(root).join("metadata.json")).unwrap(), serde_json::to_vec(i).unwrap());
656 }
657 }
658 let (mut x, mut y) = (Library::open(imm.root()).unwrap(), Library::open(bat.root()).unwrap());
660 normalise(&mut x);
661 normalise(&mut y);
662 let strip = |l: &Library| {
663 let mut m = l.metadata.clone();
664 m.folders.iter_mut().for_each(|f| {
665 f.id.clear();
666 f.modification_time = 0;
667 });
668 m.tags_groups.iter_mut().for_each(|g| g.id.clear());
669 m
670 };
671 assert_eq!(strip(&x), strip(&y));
672 assert_eq!(x.tags, y.tags);
673 assert_eq!(x.saved_filters, y.saved_filters);
674 let names = |l: &Library| l.items().iter().map(|i| (i.name.clone(), i.tags.clone(), i.is_deleted)).collect::<std::collections::BTreeSet<_>>();
675 assert_eq!(names(&x), names(&y));
676 }
677
678 #[test]
679 fn dropping_a_batched_library_flushes() {
680 let tmp = tempfile::tempdir().unwrap();
681 let (mut lib, a, _) = setup(tmp.path());
682 let root = lib.root().to_path_buf();
683 lib.set_write_mode(WriteMode::Batched).unwrap();
684 let mut i = item(&lib, &a);
685 i.tags.push("kept".into());
686 lib.update_item(i).unwrap();
687 assert!(Library::open(&root).unwrap().item(&a).unwrap().tags.is_empty());
688 drop(lib);
689 assert_eq!(Library::open(&root).unwrap().item(&a).unwrap().tags, ["kept"]);
690 }
691
692 #[test]
693 fn leaving_batched_mode_flushes_and_deleted_items_are_skipped() {
694 let tmp = tempfile::tempdir().unwrap();
695 let (mut lib, a, b) = setup(tmp.path());
696 lib.set_write_mode(WriteMode::Batched).unwrap();
697 lib.set_trashed(&[a.clone(), b.clone()], true).unwrap();
698 lib.delete_permanently(std::slice::from_ref(&a)).unwrap();
699 lib.set_write_mode(WriteMode::Immediate).unwrap();
700 let on_disk = Library::open(lib.root()).unwrap();
701 assert!(on_disk.item(&a).is_none());
702 assert!(on_disk.item(&b).unwrap().is_deleted);
703 assert_eq!(on_disk.mtime, lib.mtime);
704 }
705}