use std::io::prelude::*;
use std::{env, fs, io, path};
pub const HEADER: &[u8; 43] = b"Signature: 8a477f597d28d172789f06886806bc55";
pub fn is_tagged<P: AsRef<path::Path>>(directory: P) -> io::Result<bool> {
get_tag_state(directory).map(|state| matches!(state, TagState::Present))
}
pub fn get_tag_state<P: AsRef<path::Path>>(directory: P) -> io::Result<TagState> {
let directory = directory.as_ref();
match fs::File::open(directory.join("CACHEDIR.TAG")) {
Ok(mut cachedir_tag) => {
let mut buffer = vec![0; HEADER.len()];
let read = cachedir_tag.read(&mut buffer)?;
let header_ok = read == HEADER.len() && buffer == HEADER[..];
Ok(if header_ok {
TagState::Present
} else {
TagState::WrongHeader
})
}
Err(e) => match e.kind() {
io::ErrorKind::NotFound => {
if directory.is_dir() {
Ok(TagState::Absent)
} else {
Err(e)
}
}
_ => Err(e),
},
}
}
pub enum TagState {
Absent,
WrongHeader,
Present,
}
pub fn add_tag<P: AsRef<path::Path>>(directory: P) -> io::Result<()> {
let directory = directory.as_ref();
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(directory.join("CACHEDIR.TAG"))
{
Ok(mut cachedir_tag) => cachedir_tag.write_all(HEADER),
Err(e) => Err(e),
}
}
pub fn ensure_tag<P: AsRef<path::Path>>(directory: P) -> io::Result<()> {
match add_tag(directory) {
Err(e) => match e.kind() {
io::ErrorKind::AlreadyExists => Ok(()),
_ => Err(e),
},
other => other,
}
}
pub fn mkdir_atomic<P: AsRef<path::Path>>(directory: P) -> io::Result<bool> {
let mut directory = directory.as_ref().to_path_buf();
if directory.exists() {
return Ok(false);
}
if directory.is_relative() {
directory = env::current_dir()?.join(directory);
}
let tempdir = tempfile::Builder::new()
.prefix(directory.file_name().unwrap())
.tempdir_in(directory.parent().unwrap())?;
add_tag(tempdir.path())?;
match fs::rename(tempdir.path(), &directory) {
Ok(()) => Ok(true),
Err(e) => {
if directory.is_dir() {
Ok(false)
} else {
Err(e)
}
}
}
}
#[test]
fn is_tagged_on_nonexistent_directory_is_an_error() {
let directory = path::Path::new("this directory does not exist");
assert!(!directory.exists());
assert!(is_tagged(directory).is_err());
}
#[test]
fn empty_directory_is_not_tagged() {
assert!(!is_tagged(tempfile::tempdir().unwrap()).unwrap());
}
#[test]
fn directory_with_a_tag_with_wrong_content_is_not_tagged() {
let directory = tempfile::tempdir().unwrap();
let cachedir_tag = directory.path().join("CACHEDIR.TAG");
fs::write(&cachedir_tag, "").unwrap();
assert!(!is_tagged(&directory).unwrap());
fs::write(&cachedir_tag, &HEADER[..(HEADER.len() - 2)]).unwrap();
assert!(!is_tagged(&directory).unwrap());
}
#[test]
fn add_tag_is_detected_by_is_tagged() {
let directory = tempfile::tempdir().unwrap();
add_tag(directory.path()).unwrap();
assert!(is_tagged(directory.path()).unwrap());
}
#[test]
fn add_tag_errors_when_called_with_nonexistent_directory() {
let directory = path::Path::new("this directory does not exist");
assert!(!directory.exists());
assert!(add_tag(directory).is_err());
}
#[test]
fn add_tag_errors_when_tag_already_exists() {
let directory = tempfile::tempdir().unwrap();
assert!(add_tag(directory.path()).is_ok());
assert!(add_tag(directory.path()).is_err());
}
#[test]
fn ensure_tag_is_detected_by_is_tagged() {
let directory = tempfile::tempdir().unwrap();
ensure_tag(directory.path()).unwrap();
assert!(is_tagged(directory.path()).unwrap());
}
#[test]
fn ensure_tag_errors_when_called_with_nonexistent_directory() {
let directory = path::Path::new("this directory does not exist");
assert!(!directory.exists());
assert!(ensure_tag(directory).is_err());
assert!(is_tagged(directory).is_err());
}
#[test]
fn ensure_tag_is_idempotent() {
let directory = tempfile::tempdir().unwrap();
assert!(ensure_tag(directory.path()).is_ok());
assert!(is_tagged(directory.path()).unwrap());
assert!(ensure_tag(directory.path()).is_ok());
assert!(is_tagged(directory.path()).unwrap());
}
#[test]
fn mkdir_atomic_works() {
use std::thread;
let directory = tempfile::tempdir().unwrap();
let cache = directory.path().join("cache");
let threads = (0..10).map(|_| {
let cache = cache.clone();
thread::spawn(move || mkdir_atomic(cache))
});
let results = threads.map(|t| t.join().unwrap().unwrap());
let creations: usize = results.map(|created| if created { 1 } else { 0 }).sum();
assert_eq!(creations, 1);
assert!(is_tagged(cache).unwrap());
assert_eq!(
fs::read_dir(directory.path())
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect::<Vec<_>>(),
["cache"],
);
}