use std::borrow::Cow;
use std::fs::File;
use std::io::Error;
use std::io::ErrorKind;
use std::io::Result;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use crate::raw_cache;
use crate::trigger::PeriodicTrigger;
#[cfg(not(test))]
const MAX_TEMP_FILE_AGE: Duration = Duration::from_secs(3600);
#[cfg(test)]
const MAX_TEMP_FILE_AGE: Duration = Duration::from_secs(2);
fn ensure_directory(path: &Path) -> Result<()> {
if let Ok(meta) = std::fs::metadata(path) {
if meta.file_type().is_dir() {
return Ok(());
}
}
std::fs::create_dir_all(path)
}
fn cleanup_temporary_directory(temp_dir: Cow<Path>) -> Result<()> {
let threshold = match std::time::SystemTime::now().checked_sub(MAX_TEMP_FILE_AGE) {
Some(time) => time,
None => return Ok(()),
};
let iter = match std::fs::read_dir(&temp_dir) {
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
x => x?,
};
let mut temp = temp_dir.into_owned();
for dirent in iter.flatten() {
let mut handle = || -> Result<()> {
let metadata = dirent.metadata()?;
let mtime = metadata.modified()?;
if mtime < threshold {
temp.push(dirent.file_name());
let ret = std::fs::remove_file(&temp);
temp.pop();
ret?;
}
Ok(())
};
let _ = handle();
}
Ok(())
}
fn validate_file_name(name: &str) -> Result<&str> {
match name.as_bytes().first() {
None => Err(Error::new(
ErrorKind::InvalidInput,
"kismet cached file name must not be empty",
)),
Some(b'.') => Err(Error::new(
ErrorKind::InvalidInput,
"kismet cached file name must not start with a dot",
)),
Some(b'/') => Err(Error::new(
ErrorKind::InvalidInput,
"kismet cached file name must not starts with a forward slash",
)),
Some(b'\\') => Err(Error::new(
ErrorKind::InvalidInput,
"kismet cached file name must not starts with a backslash",
)),
Some(_) => Ok(name),
}
}
pub(crate) trait CacheDir {
fn temp_dir(&self) -> Cow<Path>;
fn base_dir(&self) -> Cow<Path>;
fn trigger(&self) -> &PeriodicTrigger;
fn capacity(&self) -> usize;
fn ensure_temp_dir(&self) -> Result<Cow<Path>> {
let ret = self.temp_dir();
ensure_directory(&ret)?;
Ok(ret)
}
fn get(&self, name: &str) -> Result<Option<File>> {
let name = validate_file_name(name)?;
let mut target = self.base_dir().into_owned();
target.push(name);
match File::open(&target) {
Ok(file) => Ok(Some(file)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn cleanup_temp_directory(&self) -> Result<()> {
cleanup_temporary_directory(self.temp_dir())
}
fn definitely_cleanup(&self, base_dir: PathBuf) -> Result<u64> {
let ret = match raw_cache::prune(base_dir, self.capacity()) {
Ok((estimate, _deleted)) => estimate,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(0),
Err(e) => return Err(e),
};
self.cleanup_temp_directory()?;
Ok(ret)
}
fn maybe_cleanup(&self, base_dir: &Path) -> Result<Option<u64>> {
if self.trigger().event() {
Ok(Some(self.definitely_cleanup(base_dir.to_owned())?))
} else {
Ok(None)
}
}
fn maintain(&self) -> Result<u64> {
self.definitely_cleanup(self.base_dir().into_owned())
}
fn set(&self, name: &str, value: &Path) -> Result<Option<u64>> {
let name = validate_file_name(name)?;
let mut dst = self.base_dir().into_owned();
let ret = self.maybe_cleanup(&dst)?;
dst.push(name);
if raw_cache::insert_or_update(value, &dst).is_ok() {
return Ok(ret);
}
std::fs::create_dir_all(dst.parent().expect("must have parent"))?;
raw_cache::insert_or_update(value, &dst)?;
Ok(ret)
}
fn put(&self, name: &str, value: &Path) -> Result<Option<u64>> {
let name = validate_file_name(name)?;
let mut dst = self.base_dir().into_owned();
let ret = self.maybe_cleanup(&dst)?;
dst.push(name);
if raw_cache::insert_or_touch(value, &dst).is_ok() {
return Ok(ret);
}
std::fs::create_dir_all(dst.parent().expect("must have parent"))?;
raw_cache::insert_or_touch(value, &dst)?;
Ok(ret)
}
fn touch(&self, name: &str) -> Result<bool> {
let name = validate_file_name(name)?;
let mut target = self.base_dir().into_owned();
target.push(name);
raw_cache::touch(&target)
}
}
#[cfg(test)]
mod test {
use std::borrow::Cow;
use std::io::ErrorKind;
use std::path::Path;
use std::path::PathBuf;
use crate::cache_dir::CacheDir;
use crate::trigger::PeriodicTrigger;
struct DummyCacheDir {}
impl CacheDir for DummyCacheDir {
#[cfg(not(tarpaulin_include))]
fn temp_dir(&self) -> Cow<Path> {
unreachable!("should not be called")
}
#[cfg(not(tarpaulin_include))]
fn base_dir(&self) -> Cow<Path> {
unreachable!("should not be called")
}
#[cfg(not(tarpaulin_include))]
fn trigger(&self) -> &PeriodicTrigger {
unreachable!("should not be called")
}
#[cfg(not(tarpaulin_include))]
fn capacity(&self) -> usize {
unreachable!("should not be called")
}
}
#[test]
fn test_bad_get() {
let cache = DummyCacheDir {};
assert!(matches!(cache.get(""),
Err(e) if e.kind() == ErrorKind::InvalidInput));
}
#[test]
fn test_bad_set() {
let cache = DummyCacheDir {};
let path: PathBuf = "/tmp/foo".into();
assert!(matches!(cache.set(".foo", &path),
Err(e) if e.kind() == ErrorKind::InvalidInput));
}
#[test]
fn test_bad_put() {
let cache = DummyCacheDir {};
let path: PathBuf = "/tmp/foo".into();
assert!(matches!(cache.set("/asd", &path),
Err(e) if e.kind() == ErrorKind::InvalidInput));
}
#[test]
fn test_bad_touch() {
let cache = DummyCacheDir {};
assert!(matches!(cache.touch("\\.test"),
Err(e) if e.kind() == ErrorKind::InvalidInput));
}
}