use crate::atomic::{publish_file_with, AtomicWriteOptions, SweepReport};
use crate::error::{FsError, Result};
use std::fs::{self, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
const MAGIC: &str = "rightkit-owned-temp 1";
const RECORD_SUFFIX: &str = ".rkowner";
const MARKER: &str = ".rkowned-";
const MAX_RECORD_BYTES: u64 = 4096;
static COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)]
#[must_use = "an unpublished OwnedTemp removes its temp when dropped"]
pub struct OwnedTemp {
target: PathBuf,
temp: PathBuf,
record: PathBuf,
done: bool,
}
impl OwnedTemp {
pub fn create(target: &Path, owner: &str) -> Result<OwnedTemp> {
if owner.is_empty() || owner.chars().any(char::is_control) {
return Err(FsError::BadName(owner.to_string()));
}
let leaf = target
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| FsError::BadName(target.display().to_string()))?;
crate::atomic::reject_reserved_name(leaf)?;
let dir = match target.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => Path::new("."),
};
let short: String = leaf.chars().take(100).collect();
for _ in 0..16 {
let name = format!(
".{short}{MARKER}{}-{}",
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed)
);
let temp = dir.join(&name);
let record = dir.join(format!("{name}{RECORD_SUFFIX}"));
let mut record_file = match OpenOptions::new()
.write(true)
.create_new(true)
.open(&record)
{
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(FsError::io(&record, e)),
};
let body = format!(
"{MAGIC}\nowner={owner}\npid={}\ntemp={name}\n",
std::process::id()
);
if let Err(e) = record_file
.write_all(body.as_bytes())
.and_then(|()| record_file.sync_all())
{
drop(record_file);
let _ = fs::remove_file(&record);
return Err(FsError::io(&record, e));
}
drop(record_file);
match OpenOptions::new().write(true).create_new(true).open(&temp) {
Ok(_) => {
return Ok(OwnedTemp {
target: target.to_path_buf(),
temp,
record,
done: false,
})
}
Err(e) => {
let _ = fs::remove_file(&record);
if e.kind() == io::ErrorKind::AlreadyExists {
continue;
}
return Err(FsError::io(&temp, e));
}
}
}
Err(FsError::AlreadyExists(target.to_path_buf()))
}
pub fn path(&self) -> &Path {
&self.temp
}
pub fn target(&self) -> &Path {
&self.target
}
pub fn publish(mut self, options: AtomicWriteOptions) -> Result<()> {
let result = publish_file_with(&self.temp, &self.target, options);
if result.is_ok() || result.as_ref().is_err_and(FsError::target_replaced) {
self.done = true;
let _ = fs::remove_file(&self.record);
}
result
}
}
impl Drop for OwnedTemp {
fn drop(&mut self) {
if !self.done {
let _ = fs::remove_file(&self.temp);
let _ = fs::remove_file(&self.record);
}
}
}
struct Record {
owner: String,
pid: u32,
temp: String,
}
fn read_record(path: &Path) -> Option<Record> {
let file = fs::File::open(path).ok()?;
if !file.metadata().ok()?.is_file() {
return None;
}
let mut text = String::new();
file.take(MAX_RECORD_BYTES).read_to_string(&mut text).ok()?;
let mut lines = text.lines();
if lines.next()? != MAGIC {
return None;
}
let (mut owner, mut pid, mut temp) = (None, None, None);
for line in lines {
if let Some(v) = line.strip_prefix("owner=") {
owner = Some(v.to_string());
} else if let Some(v) = line.strip_prefix("pid=") {
pid = v.parse::<u32>().ok();
} else if let Some(v) = line.strip_prefix("temp=") {
temp = Some(v.to_string());
}
}
Some(Record {
owner: owner?,
pid: pid?,
temp: temp?,
})
}
pub fn sweep_owned_temps(dir: &Path, owner: &str) -> Result<SweepReport> {
let mut report = SweepReport {
retained_counted: true,
..SweepReport::default()
};
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(report),
Err(e) => return Err(FsError::io(dir, e)),
};
for entry in entries.flatten() {
let file_name = entry.file_name().to_string_lossy().into_owned();
let Some(stem) = file_name.strip_suffix(RECORD_SUFFIX) else {
continue;
};
let record_path = entry.path();
let Some(record) = read_record(&record_path) else {
continue;
};
if record.owner != owner {
continue;
}
if record.temp != stem || !stem.contains(MARKER) || stem.contains(['/', '\\']) {
report.retained_other += 1;
continue;
}
if record.pid == std::process::id() {
report.retained_live += 1;
continue;
}
match crate::process_liveness(record.pid) {
crate::ProcessLiveness::Alive => {
report.retained_live += 1;
continue;
}
crate::ProcessLiveness::Unknown => {
report.retained_unknown += 1;
continue;
}
crate::ProcessLiveness::Dead => {}
}
let temp_path = dir.join(stem);
match remove_regular_file(&temp_path) {
Ok(Removal::Removed) => {
report.removed += 1;
let _ = fs::remove_file(&record_path);
}
Ok(Removal::Absent) => {
let _ = fs::remove_file(&record_path);
}
Ok(Removal::NotRegular) | Err(_) => report.retained_other += 1,
}
}
Ok(report)
}
enum Removal {
Removed,
Absent,
NotRegular,
}
fn remove_regular_file(path: &Path) -> Result<Removal> {
let before = match fs::symlink_metadata(path) {
Ok(m) => m,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Removal::Absent),
Err(e) => return Err(FsError::io(path, e)),
};
if before.file_type().is_symlink() || !before.is_file() {
return Ok(Removal::NotRegular);
}
let file = match crate::handle::open_read_nofollow(path, &crate::handle::ReadOptions::new()) {
Ok(f) => f,
Err(FsError::Symlink(_) | FsError::NotAFile(_)) => return Ok(Removal::NotRegular),
Err(e) if e.is_not_found() => return Ok(Removal::Absent),
Err(e) => return Err(e),
};
let opened = crate::file_key::FileKey::of_file(&file, path)?;
match crate::file_key::FileKey::of_path(path) {
Ok(now) if now == opened => {}
Ok(_) => return Ok(Removal::NotRegular),
Err(e) if e.is_not_found() => return Ok(Removal::Absent),
Err(e) => return Err(e),
}
drop(file);
crate::transient::retry_transient(|| fs::remove_file(path))
.map_err(|e| FsError::io(path, e))?;
Ok(Removal::Removed)
}