use std::{
io::ErrorKind,
path::Path,
sync::atomic::{AtomicU64, Ordering},
time::{Duration, SystemTime, UNIX_EPOCH},
};
const MARKS: &str = ".pending";
const CLAIMED: &str = ".claimed-";
const SWEEP_EVERY: u64 = 3600;
#[derive(Debug)]
pub(super) struct Pending {
age: Duration,
swept: AtomicU64,
}
impl Pending {
pub(super) fn new(age: Duration) -> Self {
Self {
age,
swept: AtomicU64::new(0),
}
}
pub(super) async fn mark(&self, dir: &Path, name: &str) -> std::io::Result<()> {
tokio::fs::create_dir_all(dir.join(MARKS)).await?;
tokio::fs::write(dir.join(MARKS).join(name), b"").await
}
pub(super) fn sweep_if_due(&self, dir: &Path) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs());
let last = self.swept.load(Ordering::Relaxed);
if now < last + SWEEP_EVERY
|| self
.swept
.compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
.is_err()
{
return;
}
let (dir, age) = (dir.to_path_buf(), self.age);
tokio::spawn(async move {
if let Err(error) = sweep(&dir, age).await {
tracing::error!(%error, dir = %dir.display(), "pending uploads not swept");
}
});
}
}
pub(super) async fn keep(dir: &Path, name: &str) -> std::io::Result<()> {
match tokio::fs::remove_file(dir.join(MARKS).join(name)).await {
Err(error) if error.kind() != ErrorKind::NotFound => return Err(error),
Ok(()) => return Ok(()),
Err(_) => {}
}
let claimed = tokio::fs::try_exists(dir.join(MARKS).join(format!("{CLAIMED}{name}"))).await?;
if claimed || !tokio::fs::try_exists(dir.join(name)).await? {
return Err(std::io::Error::new(
ErrorKind::NotFound,
"the upload expired; upload the file again",
));
}
Ok(())
}
pub(super) async fn unmark(dir: &Path, name: &str) -> std::io::Result<()> {
match tokio::fs::remove_file(dir.join(MARKS).join(name)).await {
Err(error) if error.kind() != ErrorKind::NotFound => Err(error),
_ => Ok(()),
}
}
async fn remove(path: &Path) -> std::io::Result<()> {
match tokio::fs::remove_file(path).await {
Err(error) if error.kind() != ErrorKind::NotFound => Err(error),
_ => Ok(()),
}
}
pub(super) async fn sweep(dir: &Path, age: Duration) -> std::io::Result<()> {
let mut marks = match tokio::fs::read_dir(dir.join(MARKS)).await {
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()),
marks => marks?,
};
while let Some(mark) = marks.next_entry().await? {
let Some(name) = mark.file_name().to_str().map(str::to_string) else {
continue;
};
if let Some(name) = name.strip_prefix(CLAIMED) {
remove(&dir.join(name)).await?;
remove(&mark.path()).await?;
continue;
}
let modified = match mark.metadata().await {
Err(error) if error.kind() == ErrorKind::NotFound => continue,
metadata => metadata?.modified()?,
};
if !modified.elapsed().is_ok_and(|elapsed| elapsed >= age) {
continue;
}
let claim = dir.join(MARKS).join(format!("{CLAIMED}{name}"));
match tokio::fs::rename(mark.path(), &claim).await {
Err(error) if error.kind() == ErrorKind::NotFound => continue,
renamed => renamed?,
}
remove(&dir.join(&name)).await?;
remove(&claim).await?;
}
Ok(())
}