use filetime::FileTime;
use std::fs::DirEntry;
use std::io::ErrorKind;
use std::io::Result;
use std::path::Path;
use std::path::PathBuf;
use crate::second_chance;
struct CachedFile {
entry: DirEntry,
mtime: FileTime,
accessed: bool,
}
fn ensure_file_removed(path: &Path) -> Result<()> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
err => err,
}
}
fn move_to_back_of_list(path: &Path) -> Result<()> {
filetime::set_file_mtime(path, FileTime::now())
}
fn set_read_only(path: &Path) -> Result<()> {
let mut permissions = std::fs::symlink_metadata(path)?.permissions();
permissions.set_readonly(true);
std::fs::set_permissions(path, permissions)
}
pub fn touch(path: impl AsRef<Path>) -> Result<bool> {
fn run(path: &Path) -> Result<bool> {
match filetime::set_file_atime(path, FileTime::now()) {
Ok(()) => Ok(true),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(false),
Err(e) => Err(e),
}
}
run(path.as_ref())
}
pub fn insert_or_update(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
fn run(from: &Path, to: &Path) -> Result<()> {
move_to_back_of_list(from)?;
set_read_only(from)?;
std::fs::rename(from, to)?;
ensure_file_removed(from)
}
run(from.as_ref(), to.as_ref())
}
pub fn insert_or_touch(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
fn run(from: &Path, to: &Path) -> Result<()> {
move_to_back_of_list(from)?;
set_read_only(from)?;
match std::fs::hard_link(from, to) {
Ok(()) => {}
Err(e) if e.kind() == ErrorKind::AlreadyExists => {
touch(to)?;
}
err => err?,
}
ensure_file_removed(from)
}
run(from.as_ref(), to.as_ref())
}
impl second_chance::Entry for CachedFile {
type Rank = FileTime;
#[inline]
fn rank(&self) -> FileTime {
self.mtime
}
#[inline]
fn accessed(&self) -> bool {
self.accessed
}
}
impl CachedFile {
fn new(entry: DirEntry, meta: &std::fs::Metadata) -> CachedFile {
let atime = FileTime::from_last_access_time(meta);
let mtime = FileTime::from_last_modification_time(meta);
CachedFile {
entry,
mtime,
accessed: atime > mtime,
}
}
}
fn collect_cached_files(cache_dir: &Path) -> Result<(Vec<CachedFile>, u64)> {
let mut cache = Vec::new();
let mut count = 0;
for maybe_entry in std::fs::read_dir(cache_dir)? {
count += 1;
if let Ok(entry) = maybe_entry {
let meta = match entry.metadata() {
Ok(meta) => meta,
Err(e) if e.kind() == ErrorKind::NotFound => continue,
err => err?,
};
if meta.is_dir() {
count -= 1;
} else {
cache.push(CachedFile::new(entry, &meta));
}
}
}
assert!(count >= cache.len() as u64);
Ok((cache, count))
}
fn apply_update(parent: PathBuf, update: second_chance::Update<CachedFile>) -> Result<()> {
let mut cached = parent;
for entry in update.to_evict {
cached.push(entry.entry.file_name());
ensure_file_removed(&cached)?;
cached.pop();
}
for entry in update.to_move_back {
cached.push(entry.entry.file_name());
match move_to_back_of_list(&cached) {
Ok(()) => {}
Err(e) if e.kind() == ErrorKind::NotFound => {}
err => err?,
}
cached.pop();
}
Ok(())
}
pub fn prune(cache_dir: PathBuf, capacity: usize) -> Result<(u64, usize)> {
let (cached_files, count) = collect_cached_files(&cache_dir)?;
let update = second_chance::Update::new(cached_files, capacity);
let num_evicted = update.to_evict.len();
assert!(num_evicted as u64 <= count);
apply_update(cache_dir, update)?;
Ok((count - (num_evicted as u64), num_evicted))
}
#[test]
fn test_remove_file() {
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("cache_file", FileType::ZeroFile(10));
let path = temp.path("cache_file");
assert!(std::fs::metadata(&path).is_ok());
assert!(ensure_file_removed(&path).is_ok());
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
assert!(ensure_file_removed(&path).is_ok());
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
}
#[test]
fn test_remove_inexistent_file() {
use test_dir::{DirBuilder, TestDir};
let temp = TestDir::temp();
let path = temp.path("cache_file");
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
assert!(ensure_file_removed(&path).is_ok());
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
}
#[cfg(test)]
fn advance_time() {
std::thread::sleep(std::time::Duration::from_secs_f64(1.5));
}
#[test]
fn test_back_of_list() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
let path = temp.path("old_cache_file");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
let old_entry = get_entry();
advance_time();
move_to_back_of_list(&path).expect("call should succeed");
let new_entry = get_entry();
assert!(new_entry.rank() > old_entry.rank());
assert!(!new_entry.accessed());
}
#[test]
fn test_set_read_only() {
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
let path = temp.path("old_cache_file");
{
let permissions = std::fs::metadata(&path)
.expect("metadata should succeed")
.permissions();
assert!(!permissions.readonly());
}
set_read_only(&path).expect("set_read_only should succeed");
{
let permissions = std::fs::metadata(&path)
.expect("metadata should succeed")
.permissions();
assert!(permissions.readonly());
}
}
#[test]
fn test_touch() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
let path = temp.path("old_cache_file");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
move_to_back_of_list(&path).expect("call should succeed");
let old_entry = get_entry();
assert!(!old_entry.accessed());
advance_time();
assert!(touch(&path).expect("call should succeed"));
let new_entry = get_entry();
assert_eq!(new_entry.rank(), old_entry.rank());
assert!(new_entry.accessed());
}
#[test]
fn test_touch_missing() {
use test_dir::{DirBuilder, TestDir};
let temp = TestDir::temp();
assert!(!touch(&temp.path("absent")).expect("should succeed on missing files"));
}
#[test]
fn test_touch_by_open() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
let path = temp.path("old_cache_file");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
move_to_back_of_list(&path).expect("call should succeed");
let old_entry = get_entry();
assert!(!old_entry.accessed());
advance_time();
let _ = std::fs::read(&path).expect("read should succeed");
let new_entry = get_entry();
assert_eq!(new_entry.rank(), old_entry.rank());
assert!(new_entry.accessed());
}
#[test]
fn test_back_of_list_after_touch() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
let path = temp.path("old_cache_file");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
advance_time();
touch(&path).expect("call should succeed");
let old_entry = get_entry();
assert!(old_entry.accessed());
advance_time();
move_to_back_of_list(&path).expect("call should succeed");
let new_entry = get_entry();
assert!(new_entry.rank() > old_entry.rank());
assert!(!new_entry.accessed());
}
#[test]
fn test_back_of_list_after_open() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("old_cache_file", FileType::ZeroFile(10));
let path = temp.path("old_cache_file");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
advance_time();
let _ = std::fs::read(&path).expect("read should succeed");
let old_entry = get_entry();
assert!(old_entry.accessed());
advance_time();
move_to_back_of_list(&path).expect("call should succeed");
let new_entry = get_entry();
assert!(new_entry.rank() > old_entry.rank());
assert!(!new_entry.accessed());
}
#[test]
fn test_insert_empty() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("temp_file", FileType::RandomFile(10));
let path = temp.path("temp_file");
let dst = temp.path("cache");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
advance_time();
let payload = std::fs::read(&path).expect("read should succeed");
insert_or_update(&path, &dst).expect("insert_or_touch should succeed");
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
assert!(!get_entry().accessed());
assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
assert!(std::fs::metadata(&dst)
.expect("metadata should succeed")
.permissions()
.readonly());
}
#[test]
fn test_insert_overwrite() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp()
.create("temp_file", FileType::RandomFile(10))
.create("cache", FileType::ZeroFile(100));
let path = temp.path("temp_file");
let dst = temp.path("cache");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
advance_time();
let payload = std::fs::read(&path).expect("read should succeed");
insert_or_update(&path, &dst).expect("insert_or_touch should succeed");
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
assert!(!get_entry().accessed());
assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
}
#[test]
fn test_insert_or_touch_empty() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp().create("temp_file", FileType::RandomFile(10));
let path = temp.path("temp_file");
let dst = temp.path("cache");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
advance_time();
let payload = std::fs::read(&path).expect("read should succeed");
insert_or_touch(&path, &dst).expect("insert_or_touch should succeed");
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
assert!(!get_entry().accessed());
assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
assert!(std::fs::metadata(&dst)
.expect("metadata should succeed")
.permissions()
.readonly());
}
#[test]
fn test_insert_touch_overwrite() {
use crate::second_chance::Entry;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp()
.create("temp_file", FileType::RandomFile(10))
.create("cache", FileType::RandomFile(10));
let path = temp.path("temp_file");
let dst = temp.path("cache");
let get_entry = || {
let (mut files, count) =
collect_cached_files(path.parent().unwrap()).expect("directory listing must succeed");
assert_eq!(count, 1);
assert_eq!(files.len(), 1);
files.pop().expect("vec is non-empty")
};
let payload = std::fs::read(&dst).expect("read should succeed");
move_to_back_of_list(&dst).expect("move to back should succeed");
advance_time();
insert_or_touch(&path, &dst).expect("insert_or_touch should succeed");
assert!(matches!(std::fs::metadata(&path),
Err(e) if e.kind() == ErrorKind::NotFound));
assert!(get_entry().accessed());
assert_eq!(&payload, &std::fs::read(&dst).expect("read should succeed"));
}
#[test]
fn test_collect_cached_files() {
use crate::second_chance::Entry;
use std::ffi::OsString;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp()
.create("a", FileType::RandomFile(10))
.create("b", FileType::RandomFile(10))
.create("c", FileType::RandomFile(10))
.create("d", FileType::RandomFile(10))
.create("a_directory", FileType::Dir);
advance_time();
move_to_back_of_list(&temp.path("d")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("a")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("c")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("b")).expect("should succeed");
advance_time();
touch(&temp.path("d")).expect("should succeed");
touch(&temp.path("c")).expect("should succeed");
let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
assert_eq!(count, 4);
cached.sort_by_key(|e| e.rank());
assert_eq!(
cached
.iter()
.map(|e| e.entry.file_name())
.collect::<Vec<OsString>>(),
vec!["d", "a", "c", "b"]
);
assert_eq!(
cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
vec![true, false, true, false]
);
}
#[test]
fn test_apply_update() {
use crate::second_chance::Entry;
use std::ffi::OsString;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp()
.create("a", FileType::RandomFile(10))
.create("b", FileType::RandomFile(10))
.create("c", FileType::RandomFile(10))
.create("d", FileType::RandomFile(10))
.create("deleted_touch", FileType::RandomFile(10))
.create("already_deleted", FileType::RandomFile(10));
advance_time();
move_to_back_of_list(&temp.path("d")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("a")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("c")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("b")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("deleted_touch")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("already_deleted")).expect("should succeed");
advance_time();
touch(&temp.path("d")).expect("should succeed");
touch(&temp.path("c")).expect("should succeed");
{
let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
assert_eq!(count, 6);
cached.sort_by_key(|e| e.rank());
assert_eq!(
cached
.iter()
.map(|e| e.entry.file_name())
.collect::<Vec<OsString>>(),
vec!["d", "a", "c", "b", "deleted_touch", "already_deleted"]
);
let mut to_evict = Vec::new();
let mut to_move_back = Vec::new();
to_move_back.extend(cached.drain(0..1));
to_evict.extend(cached.drain(0..1));
to_evict.push(cached.pop().expect("is non-empty"));
to_move_back.push(cached.pop().expect("is non-empty"));
std::fs::remove_file(&temp.path("deleted_touch")).expect("deletion should succeed");
std::fs::remove_file(&temp.path("already_deleted")).expect("deletion should succeed");
apply_update(
temp.path("."),
second_chance::Update {
to_evict,
to_move_back,
},
)
.expect("update should succeed");
}
let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
assert_eq!(count, 3);
cached.sort_by_key(|e| e.rank());
assert_eq!(
cached
.iter()
.map(|e| e.entry.file_name())
.collect::<Vec<OsString>>(),
vec!["c", "b", "d"]
);
assert_eq!(
cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
vec![true, false, false]
);
}
#[test]
fn test_prune() {
use crate::second_chance::Entry;
use std::ffi::OsString;
use test_dir::{DirBuilder, FileType, TestDir};
let temp = TestDir::temp()
.create("a", FileType::RandomFile(10))
.create("b", FileType::RandomFile(10))
.create("c", FileType::RandomFile(10))
.create("d", FileType::RandomFile(10));
advance_time();
move_to_back_of_list(&temp.path("d")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("a")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("c")).expect("should succeed");
advance_time();
move_to_back_of_list(&temp.path("b")).expect("should succeed");
advance_time();
touch(&temp.path("d")).expect("should succeed");
touch(&temp.path("c")).expect("should succeed");
assert_eq!(
prune(temp.path("."), 3).expect("prune should succeed"),
(3, 1)
);
let (mut cached, count) = collect_cached_files(&temp.path(".")).expect("should succeed");
assert_eq!(count, 3);
cached.sort_by_key(|e| e.rank());
assert_eq!(
cached
.iter()
.map(|e| e.entry.file_name())
.collect::<Vec<OsString>>(),
vec!["c", "b", "d"]
);
assert_eq!(
cached.iter().map(|e| e.accessed()).collect::<Vec<bool>>(),
vec![true, false, false]
);
}