1use filetime::FileTime;
14use std::fs::DirEntry;
15use std::io::ErrorKind;
16use std::io::Result;
17use std::path::Path;
18use std::path::PathBuf;
19
20use crate::second_chance;
21
22const ENFORCED_ATIME_MTIME_DELTA_SEC: i64 = 120;
31
32struct CachedFile {
35 entry: DirEntry,
36 mtime: FileTime,
37 accessed: bool,
38}
39
40fn ensure_file_removed(path: &Path) -> Result<()> {
42 match std::fs::remove_file(path) {
43 Ok(()) => Ok(()),
44 Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
45 err => err,
46 }
47}
48
49fn move_to_back_of_list(path: &Path) -> Result<()> {
51 let mtime = FileTime::now();
52 let atime = FileTime::from_unix_time(
56 mtime
57 .unix_seconds()
58 .saturating_sub(ENFORCED_ATIME_MTIME_DELTA_SEC),
59 mtime.nanoseconds(),
60 );
61 filetime::set_file_times(path, atime, mtime)
62}
63
64fn set_read_only(path: &Path) -> Result<()> {
66 let mut permissions = std::fs::symlink_metadata(path)?.permissions();
67
68 permissions.set_readonly(true);
69 std::fs::set_permissions(path, permissions)
70}
71
72pub fn touch(path: impl AsRef<Path>) -> Result<bool> {
80 fn run(path: &Path) -> Result<bool> {
81 match filetime::set_file_atime(path, FileTime::now()) {
82 Ok(()) => Ok(true),
83 Err(e) if e.kind() == ErrorKind::NotFound => Ok(false),
86 Err(e) => Err(e),
87 }
88 }
89
90 run(path.as_ref())
91}
92
93pub fn insert_or_update(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
107 fn run(from: &Path, to: &Path) -> Result<()> {
108 move_to_back_of_list(from)?;
111 set_read_only(from)?;
112 std::fs::rename(from, to)?;
113 ensure_file_removed(from)
114 }
115
116 run(from.as_ref(), to.as_ref())
117}
118
119pub fn insert_or_touch(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
137 fn run(from: &Path, to: &Path) -> Result<()> {
138 move_to_back_of_list(from)?;
142 set_read_only(from)?;
143 match std::fs::hard_link(from, to) {
144 Ok(()) => {}
145 Err(e) if e.kind() == ErrorKind::AlreadyExists => {
148 touch(to)?;
149 }
150 err => err?,
151 }
152
153 ensure_file_removed(from)
154 }
155
156 run(from.as_ref(), to.as_ref())
157}
158
159impl second_chance::Entry for CachedFile {
160 type Rank = FileTime;
161
162 #[inline]
163 fn rank(&self) -> FileTime {
164 self.mtime
165 }
166
167 #[inline]
168 fn accessed(&self) -> bool {
169 self.accessed
170 }
171}
172
173impl CachedFile {
174 fn new(entry: DirEntry, meta: &std::fs::Metadata) -> CachedFile {
175 let atime = FileTime::from_last_access_time(meta);
176 let mtime = FileTime::from_last_modification_time(meta);
177
178 CachedFile {
179 entry,
180 mtime,
181 accessed: atime > mtime,
182 }
183 }
184}
185
186fn collect_cached_files(cache_dir: &Path) -> Result<(Vec<CachedFile>, u64)> {
194 let mut cache = Vec::new();
195 let mut count = 0;
196 for maybe_entry in std::fs::read_dir(cache_dir)? {
197 count += 1;
198 if let Ok(entry) = maybe_entry {
199 let meta = match entry.metadata() {
200 Ok(meta) => meta,
201 Err(e) if e.kind() == ErrorKind::NotFound => continue,
202 err => err?,
203 };
204
205 if meta.is_dir() {
206 count -= 1;
208 } else {
209 cache.push(CachedFile::new(entry, &meta));
210 }
211 }
212 }
213
214 assert!(count >= cache.len() as u64);
216 Ok((cache, count))
217}
218
219fn apply_update(parent: PathBuf, update: second_chance::Update<CachedFile>) -> Result<()> {
221 let mut cached = parent;
225
226 for entry in update.to_evict {
227 cached.push(entry.entry.file_name());
228 ensure_file_removed(&cached)?;
229 cached.pop();
230 }
231
232 for entry in update.to_move_back {
233 cached.push(entry.entry.file_name());
234 match move_to_back_of_list(&cached) {
235 Ok(()) => {}
236 Err(e) if e.kind() == ErrorKind::NotFound => {}
238 err => err?,
239 }
240 cached.pop();
241 }
242
243 Ok(())
244}
245
246pub fn prune(cache_dir: PathBuf, capacity: usize) -> Result<(u64, usize)> {
254 let (cached_files, count) = collect_cached_files(&cache_dir)?;
255 let update = second_chance::Update::new(cached_files, capacity);
256 let num_evicted = update.to_evict.len();
257
258 assert!(num_evicted as u64 <= count);
263
264 apply_update(cache_dir, update)?;
265 Ok((count - (num_evicted as u64), num_evicted))
266}
267
268#[test]
270fn test_remove_file() {
271 use test_dir::{DirBuilder, FileType, TestDir};
272 let temp = TestDir::temp().create("cache_file", FileType::ZeroFile(10));
273
274 let path = temp.path("cache_file");
275 assert!(std::fs::metadata(&path).is_ok());
276 assert!(ensure_file_removed(&path).is_ok());
277
278 assert!(matches!(std::fs::metadata(&path),
279 Err(e) if e.kind() == ErrorKind::NotFound));
280
281 assert!(ensure_file_removed(&path).is_ok());
283 assert!(matches!(std::fs::metadata(&path),
284 Err(e) if e.kind() == ErrorKind::NotFound));
285}
286
287#[test]
289fn test_remove_inexistent_file() {
290 use test_dir::{DirBuilder, TestDir};
291 let temp = TestDir::temp();
292
293 let path = temp.path("cache_file");
294 assert!(matches!(std::fs::metadata(&path),
295 Err(e) if e.kind() == ErrorKind::NotFound));
296
297 assert!(ensure_file_removed(&path).is_ok());
299 assert!(matches!(std::fs::metadata(&path),
300 Err(e) if e.kind() == ErrorKind::NotFound));
301}
302
303#[cfg(test)]
306fn advance_time() {
307 std::thread::sleep(std::time::Duration::from_secs_f64(1.5));
308}
309
310#[test]
313fn test_back_of_list() {
314 use crate::second_chance::Entry;
315 use test_dir::{DirBuilder, FileType, TestDir};
316
317 let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
318
319 let path = temp.path("old_cache_file");
320
321 let get_entry = || {
322 let (mut files, count) =
323 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
324 assert_eq!(count, 1);
325 assert_eq!(files.len(), 1);
326
327 files.pop().expect("vec is non-empty")
328 };
329
330 let old_entry = get_entry();
331 advance_time();
332 move_to_back_of_list(&path).expect("call should succeed");
333 let new_entry = get_entry();
334
335 assert!(new_entry.rank() > old_entry.rank());
336 assert!(!new_entry.accessed());
337}
338
339#[test]
341fn test_set_read_only() {
342 use test_dir::{DirBuilder, FileType, TestDir};
343
344 let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
345
346 let path = temp.path("old_cache_file");
347
348 {
350 let permissions = std::fs::metadata(&path)
351 .expect("metadata should succeed")
352 .permissions();
353
354 assert!(!permissions.readonly());
355 }
356
357 set_read_only(&path).expect("set_read_only should succeed");
358
359 {
361 let permissions = std::fs::metadata(&path)
362 .expect("metadata should succeed")
363 .permissions();
364
365 assert!(permissions.readonly());
366 }
367}
368
369#[test]
371fn test_touch() {
372 use crate::second_chance::Entry;
373 use test_dir::{DirBuilder, FileType, TestDir};
374
375 let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
376
377 let path = temp.path("old_cache_file");
378
379 let get_entry = || {
380 let (mut files, count) =
381 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
382 assert_eq!(count, 1);
383 assert_eq!(files.len(), 1);
384
385 files.pop().expect("vec is non-empty")
386 };
387
388 move_to_back_of_list(&path).expect("call should succeed");
389 let old_entry = get_entry();
390 assert!(!old_entry.accessed());
391
392 advance_time();
393 assert!(touch(&path).expect("call should succeed"));
395 let new_entry = get_entry();
396
397 assert_eq!(new_entry.rank(), old_entry.rank());
398 assert!(new_entry.accessed());
399}
400
401#[test]
403fn test_touch_missing() {
404 use test_dir::{DirBuilder, TestDir};
405
406 let temp = TestDir::temp();
407 assert!(!touch(&temp.path("absent")).expect("should succeed on missing files"));
409}
410
411#[test]
413fn test_touch_by_open() {
414 use crate::second_chance::Entry;
415 use test_dir::{DirBuilder, FileType, TestDir};
416
417 let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
418
419 let path = temp.path("old_cache_file");
420
421 let get_entry = || {
422 let (mut files, count) =
423 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
424 assert_eq!(count, 1);
425 assert_eq!(files.len(), 1);
426
427 files.pop().expect("vec is non-empty")
428 };
429
430 move_to_back_of_list(&path).expect("call should succeed");
431 let old_entry = get_entry();
432 assert!(!old_entry.accessed());
433
434 advance_time();
435 let _ = std::fs::read(&path).expect("read should succeed");
436 let new_entry = get_entry();
437
438 assert_eq!(new_entry.rank(), old_entry.rank());
439 assert!(new_entry.accessed());
440}
441
442#[test]
444fn test_back_of_list_after_touch() {
445 use crate::second_chance::Entry;
446 use test_dir::{DirBuilder, FileType, TestDir};
447
448 let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
449
450 let path = temp.path("old_cache_file");
451
452 let get_entry = || {
453 let (mut files, count) =
454 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
455 assert_eq!(count, 1);
456 assert_eq!(files.len(), 1);
457
458 files.pop().expect("vec is non-empty")
459 };
460
461 advance_time();
462 touch(&path).expect("call should succeed");
463 let old_entry = get_entry();
464 assert!(old_entry.accessed());
465
466 advance_time();
467 move_to_back_of_list(&path).expect("call should succeed");
468 let new_entry = get_entry();
469
470 assert!(new_entry.rank() > old_entry.rank());
471 assert!(!new_entry.accessed());
472}
473
474#[test]
477fn test_back_of_list_after_open() {
478 use crate::second_chance::Entry;
479 use test_dir::{DirBuilder, FileType, TestDir};
480
481 let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
482
483 let path = temp.path("old_cache_file");
484
485 let get_entry = || {
486 let (mut files, count) =
487 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
488 assert_eq!(count, 1);
489 assert_eq!(files.len(), 1);
490
491 files.pop().expect("vec is non-empty")
492 };
493
494 advance_time();
495 let _ = std::fs::read(&path).expect("read should succeed");
496 let old_entry = get_entry();
497 assert!(old_entry.accessed());
498
499 advance_time();
500 move_to_back_of_list(&path).expect("call should succeed");
501 let new_entry = get_entry();
502
503 assert!(new_entry.rank() > old_entry.rank());
504 assert!(!new_entry.accessed());
505}
506
507#[test]
510fn test_insert_empty() {
511 use crate::second_chance::Entry;
512 use test_dir::{DirBuilder, FileType, TestDir};
513
514 let temp = TestDir::temp().create("temp_file", FileType::RandomFile(10));
515
516 let path = temp.path("temp_file");
517 let dst = temp.path("cache");
518
519 let get_entry = || {
520 let (mut files, count) =
521 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
522 assert_eq!(count, 1);
523 assert_eq!(files.len(), 1);
524
525 files.pop().expect("vec is non-empty")
526 };
527
528 advance_time();
531 let payload = std::fs::read(&path).expect("read should succeed");
532
533 insert_or_update(&path, &dst).expect("insert_or_touch should succeed");
534 assert!(matches!(std::fs::metadata(&path),
536 Err(e) if e.kind() == ErrorKind::NotFound));
537
538 assert!(!get_entry().accessed());
540 assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
542 assert!(std::fs::metadata(&dst)
544 .expect("metadata should succeed")
545 .permissions()
546 .readonly());
547}
548
549#[test]
552fn test_insert_overwrite() {
553 use crate::second_chance::Entry;
554 use test_dir::{DirBuilder, FileType, TestDir};
555
556 let temp = TestDir::temp()
557 .create("temp_file", FileType::RandomFile(10))
558 .create("cache", FileType::ZeroFile(100));
559
560 let path = temp.path("temp_file");
561 let dst = temp.path("cache");
562
563 let get_entry = || {
564 let (mut files, count) =
565 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
566 assert_eq!(count, 1);
567 assert_eq!(files.len(), 1);
568
569 files.pop().expect("vec is non-empty")
570 };
571
572 advance_time();
575 let payload = std::fs::read(&path).expect("read should succeed");
576
577 insert_or_update(&path, &dst).expect("insert_or_touch should succeed");
578 assert!(matches!(std::fs::metadata(&path),
580 Err(e) if e.kind() == ErrorKind::NotFound));
581
582 assert!(!get_entry().accessed());
584 assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
586}
587
588#[test]
591fn test_insert_or_touch_empty() {
592 use crate::second_chance::Entry;
593 use test_dir::{DirBuilder, FileType, TestDir};
594
595 let temp = TestDir::temp().create("temp_file", FileType::RandomFile(10));
596
597 let path = temp.path("temp_file");
598 let dst = temp.path("cache");
599
600 let get_entry = || {
601 let (mut files, count) =
602 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
603 assert_eq!(count, 1);
604 assert_eq!(files.len(), 1);
605
606 files.pop().expect("vec is non-empty")
607 };
608
609 advance_time();
612 let payload = std::fs::read(&path).expect("read should succeed");
613
614 insert_or_touch(&path, &dst).expect("insert_or_touch should succeed");
615 assert!(matches!(std::fs::metadata(&path),
617 Err(e) if e.kind() == ErrorKind::NotFound));
618
619 assert!(!get_entry().accessed());
621 assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
623 assert!(std::fs::metadata(&dst)
625 .expect("metadata should succeed")
626 .permissions()
627 .readonly());
628}
629
630#[test]
633fn test_insert_touch_overwrite() {
634 use crate::second_chance::Entry;
635 use test_dir::{DirBuilder, FileType, TestDir};
636
637 let temp = TestDir::temp()
638 .create("temp_file", FileType::RandomFile(10))
639 .create("cache", FileType::RandomFile(10));
640
641 let path = temp.path("temp_file");
642 let dst = temp.path("cache");
643
644 let get_entry = || {
645 let (mut files, count) =
646 collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
647 assert_eq!(count, 1);
648 assert_eq!(files.len(), 1);
649
650 files.pop().expect("vec is non-empty")
651 };
652
653 let payload = std::fs::read(&dst).expect("read should succeed");
654 move_to_back_of_list(&dst).expect("move to back should succeed");
656
657 advance_time();
658
659 insert_or_touch(&path, &dst).expect("insert_or_touch should succeed");
660 assert!(matches!(std::fs::metadata(&path),
662 Err(e) if e.kind() == ErrorKind::NotFound));
663
664 assert!(get_entry().accessed());
666 assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
668}
669
670#[test]
672fn test_collect_cached_files() {
673 use crate::second_chance::Entry;
674 use std::ffi::OsString;
675 use test_dir::{DirBuilder, FileType, TestDir};
676
677 let temp = TestDir::temp()
678 .create("a", FileType::RandomFile(10))
679 .create("b", FileType::RandomFile(10))
680 .create("c", FileType::RandomFile(10))
681 .create("d", FileType::RandomFile(10))
682 .create("a_directory", FileType::Dir);
684
685 advance_time();
687 move_to_back_of_list(&temp.path("d")).expect("should succeed");
688 advance_time();
689 move_to_back_of_list(&temp.path("a")).expect("should succeed");
690 advance_time();
691 move_to_back_of_list(&temp.path("c")).expect("should succeed");
692 advance_time();
693 move_to_back_of_list(&temp.path("b")).expect("should succeed");
694
695 advance_time();
697 touch(&temp.path("d")).expect("should succeed");
698 touch(&temp.path("c")).expect("should succeed");
699
700 let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
701
702 assert_eq!(count, 4);
703 cached.sort_by_key(|e| e.rank());
704
705 assert_eq!(
706 cached
707 .iter()
708 .map(|e| e.entry.file_name())
709 .collect::<Vec<OsString>>(),
710 vec!["d", "a", "c", "b"]
711 );
712
713 assert_eq!(
714 cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
715 vec![true, false, true, false]
716 );
717}
718
719#[test]
721fn test_apply_update() {
722 use crate::second_chance::Entry;
723 use std::ffi::OsString;
724 use test_dir::{DirBuilder, FileType, TestDir};
725
726 let temp = TestDir::temp()
727 .create("a", FileType::RandomFile(10))
728 .create("b", FileType::RandomFile(10))
729 .create("c", FileType::RandomFile(10))
730 .create("d", FileType::RandomFile(10))
731 .create("deleted_touch", FileType::RandomFile(10))
734 .create("already_deleted", FileType::RandomFile(10));
735
736 advance_time();
738 move_to_back_of_list(&temp.path("d")).expect("should succeed");
739 advance_time();
740 move_to_back_of_list(&temp.path("a")).expect("should succeed");
741 advance_time();
742 move_to_back_of_list(&temp.path("c")).expect("should succeed");
743 advance_time();
744 move_to_back_of_list(&temp.path("b")).expect("should succeed");
745
746 advance_time();
748 move_to_back_of_list(&temp.path("deleted_touch")).expect("should succeed");
749 advance_time();
750 move_to_back_of_list(&temp.path("already_deleted")).expect("should succeed");
751
752 advance_time();
754 touch(&temp.path("d")).expect("should succeed");
755 touch(&temp.path("c")).expect("should succeed");
756
757 {
759 let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
760
761 assert_eq!(count, 6);
762 cached.sort_by_key(|e| e.rank());
763
764 assert_eq!(
765 cached
766 .iter()
767 .map(|e| e.entry.file_name())
768 .collect::<Vec<OsString>>(),
769 vec!["d", "a", "c", "b", "deleted_touch", "already_deleted"]
770 );
771
772 let mut to_evict = Vec::new();
773 let mut to_move_back = Vec::new();
774
775 to_move_back.extend(cached.drain(0..1));
776 to_evict.extend(cached.drain(0..1));
777 to_evict.push(cached.pop().expect("is non-empty"));
778 to_move_back.push(cached.pop().expect("is non-empty"));
779
780 std::fs::remove_file(&temp.path("deleted_touch")).expect("deletion should succeed");
783 std::fs::remove_file(&temp.path("already_deleted")).expect("deletion should succeed");
784
785 apply_update(
786 temp.path("."),
787 second_chance::Update {
788 to_evict,
789 to_move_back,
790 },
791 )
792 .expect("update should succeed");
793 }
794
795 let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
796
797 assert_eq!(count, 3);
798 cached.sort_by_key(|e| e.rank());
799
800 assert_eq!(
802 cached
803 .iter()
804 .map(|e| e.entry.file_name())
805 .collect::<Vec<OsString>>(),
806 vec!["c", "b", "d"]
807 );
808
809 assert_eq!(
811 cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
812 vec![true, false, false]
813 );
814}
815
816#[test]
818fn test_prune() {
819 use crate::second_chance::Entry;
820 use std::ffi::OsString;
821 use test_dir::{DirBuilder, FileType, TestDir};
822
823 let temp = TestDir::temp()
824 .create("a", FileType::RandomFile(10))
825 .create("b", FileType::RandomFile(10))
826 .create("c", FileType::RandomFile(10))
827 .create("d", FileType::RandomFile(10));
828
829 advance_time();
831 move_to_back_of_list(&temp.path("d")).expect("should succeed");
832 advance_time();
833 move_to_back_of_list(&temp.path("a")).expect("should succeed");
834 advance_time();
835 move_to_back_of_list(&temp.path("c")).expect("should succeed");
836 advance_time();
837 move_to_back_of_list(&temp.path("b")).expect("should succeed");
838
839 advance_time();
841 touch(&temp.path("d")).expect("should succeed");
842 touch(&temp.path("c")).expect("should succeed");
843
844 assert_eq!(
849 prune(temp.path("."), 3).expect("prune should succeed"),
850 (3, 1)
851 );
852
853 let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
854
855 assert_eq!(count, 3);
856 cached.sort_by_key(|e| e.rank());
857
858 assert_eq!(
860 cached
861 .iter()
862 .map(|e| e.entry.file_name())
863 .collect::<Vec<OsString>>(),
864 vec!["c", "b", "d"]
865 );
866
867 assert_eq!(
869 cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
870 vec![true, false, false]
871 );
872}