use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)]
pub struct ScratchDir {
path: PathBuf,
keep: bool,
}
impl ScratchDir {
pub fn new(tag: &str) -> io::Result<Self> {
let base = std::env::temp_dir();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default();
let mut last_err = None;
for attempt in 0..8u64 {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let path = base.join(format!(
"powercut-{}-{}-{nanos:x}-{n}-{attempt}",
sanitise(tag),
std::process::id(),
));
match std::fs::create_dir(&path) {
Ok(()) => {
return Ok(Self {
path,
keep: std::env::var_os("POWERCUT_KEEP").is_some(),
})
}
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or_else(|| io::Error::other("could not create a scratch directory")))
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn keep(mut self) -> PathBuf {
self.keep = true;
self.path.clone()
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
if self.keep {
eprintln!("powercut: keeping {}", self.path.display());
} else {
let _ = std::fs::remove_dir_all(&self.path);
}
}
}
fn sanitise(tag: &str) -> String {
tag.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect()
}
pub(crate) fn copy_file(src: &Path, dst: &Path, limit: Option<u64>) -> io::Result<u64> {
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?;
}
let copied = std::fs::copy(src, dst)?;
match limit {
Some(limit) if limit < copied => {
let file = std::fs::OpenOptions::new().write(true).open(dst)?;
file.set_len(limit)?;
Ok(limit)
}
_ => Ok(copied),
}
}
pub(crate) fn walk_files(root: &Path) -> io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
let mut files = Vec::new();
let mut skipped = Vec::new();
walk_into(root, root, &mut files, &mut skipped)?;
files.sort();
skipped.sort();
Ok((files, skipped))
}
fn walk_into(
root: &Path,
dir: &Path,
files: &mut Vec<PathBuf>,
skipped: &mut Vec<PathBuf>,
) -> io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let meta = entry.metadata()?;
let relative = path.strip_prefix(root).unwrap_or(&path).to_path_buf();
if meta.is_symlink() {
skipped.push(relative);
} else if meta.is_dir() {
walk_into(root, &path, files, skipped)?;
} else if meta.is_file() {
files.push(relative);
} else {
skipped.push(relative);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn walk_finds_nested_files_and_skips_symlinks() {
let dir = ScratchDir::new("walk").unwrap();
std::fs::create_dir_all(dir.path().join("sub")).unwrap();
std::fs::write(dir.path().join("a"), b"a").unwrap();
std::fs::write(dir.path().join("sub/b"), b"b").unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink(dir.path().join("a"), dir.path().join("link")).unwrap();
let (files, skipped) = walk_files(dir.path()).unwrap();
assert_eq!(files, vec![PathBuf::from("a"), PathBuf::from("sub/b")]);
#[cfg(unix)]
assert_eq!(skipped, vec![PathBuf::from("link")]);
#[cfg(not(unix))]
assert!(skipped.is_empty());
}
#[test]
fn copy_truncates_to_the_durable_length() {
let dir = ScratchDir::new("truncate").unwrap();
let src = dir.path().join("src");
let dst = dir.path().join("dst");
std::fs::write(&src, b"0123456789").unwrap();
assert_eq!(copy_file(&src, &dst, Some(4)).unwrap(), 4);
assert_eq!(std::fs::read(&dst).unwrap(), b"0123");
}
}