1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-10-09 21:03:17 +02:00
archivr/crates/archivr-server/src/main.rs
2026-08-24 15:31:04 +02:00

171 lines
6.7 KiB
Rust

mod auth;
mod registry;
mod routes;
use anyhow::{Context, Result};
use std::{net::SocketAddr, path::PathBuf};
const DEFAULT_BIND: &str = "127.0.0.1:8080";
/// Prune abandoned staged-upload UUID dirs under `uploads_dir` whose content
/// is older than `cutoff`.
///
/// `cleanup_stale_sentinels`: pass `true` at startup (before the server begins
/// accepting connections) so crash-leftover `.uploading` markers are removed and
/// those dirs are subject to the normal age check. Pass `false` from the
/// in-process periodic task so dirs with a live sentinel (active XHR) are
/// skipped entirely.
///
/// Staleness is measured from the newest non-sentinel child file's mtime so a
/// just-finished slow upload is not pruned before the user submits it for
/// capture. Empty dirs fall back to the directory mtime.
fn prune_stale_upload_dirs(
uploads_dir: &std::path::Path,
cutoff: std::time::SystemTime,
cleanup_stale_sentinels: bool,
) {
let Ok(entries) = std::fs::read_dir(uploads_dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let sentinel = path.join(".uploading");
if sentinel.exists() {
if cleanup_stale_sentinels {
// Server just started — no uploads are in flight, so any
// sentinel is a crash remnant. Remove it and fall through
// to the age check below.
let _ = std::fs::remove_file(&sentinel);
} else {
// An active XHR is writing to this dir — leave it alone.
continue;
}
}
// Measure staleness from the newest non-sentinel child file so a
// completed slow upload isn't pruned while the user is still on the
// capture form. Fall back to dir mtime only when the dir is empty.
let newest_child = std::fs::read_dir(&path)
.ok()
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.filter(|e| e.file_name() != std::ffi::OsStr::new(".uploading"))
.filter_map(|e| e.metadata().ok())
.filter_map(|m| m.modified().ok())
.max();
let reference = newest_child
.or_else(|| entry.metadata().and_then(|m| m.modified()).ok())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
if reference < cutoff {
let _ = std::fs::remove_dir_all(&path);
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let config_path = std::env::args()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("archivr-server.toml"));
let registry = registry::load_registry(&config_path)?;
// Auth DB lives next to the config file unless overridden in the TOML.
let auth_db_path = registry.auth_db_path.clone().unwrap_or_else(|| {
config_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join("archivr-auth.sqlite")
});
let app = routes::app(registry.clone(), auth_db_path.clone());
// On startup, mark any jobs that were 'running' when the server last stopped as 'failed'.
for archive in &registry.archives {
if let Ok(conn) = archivr_core::database::open_or_initialize(&archive.archive_path) {
match archivr_core::database::fail_stalled_capture_jobs(&conn) {
Ok(n) if n > 0 => eprintln!(
"info: marked {n} stalled capture job(s) as failed in '{}'",
archive.id
),
Err(e) => eprintln!(
"warn: stalled job cleanup failed for '{}': {e:#}",
archive.id
),
_ => {}
}
match archivr_core::database::fail_stalled_entry_summaries(&conn) {
Ok(n) if n > 0 => eprintln!(
"info: marked {n} stalled summary attempt(s) as failed in '{}'",
archive.id
),
Err(e) => eprintln!(
"warn: stalled summary cleanup failed for '{}': {e:#}",
archive.id
),
_ => {}
}
}
}
// Prune staged upload dirs older than 24 h. cleanup_stale_sentinels=true
// because no uploads are in flight before the server starts listening.
let prune_cutoff = std::time::SystemTime::now()
.checked_sub(std::time::Duration::from_secs(24 * 60 * 60))
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
for archive in &registry.archives {
if let Ok(paths) = archivr_core::archive::read_archive_paths(&archive.archive_path) {
let uploads_dir = paths.store_path.join("temp").join("uploads");
prune_stale_upload_dirs(&uploads_dir, prune_cutoff, true);
}
}
// Spawn maintenance task: session cleanup + staged-upload pruning, every 24 h.
let cleanup_auth_path = auth_db_path.clone();
let cleanup_registry = registry.clone();
tokio::spawn(async move {
loop {
if let Ok(conn) = archivr_core::database::open_auth_db(&cleanup_auth_path) {
match archivr_core::database::delete_expired_sessions(&conn) {
Ok(n) if n > 0 => eprintln!("info: cleaned up {n} expired sessions"),
Err(e) => eprintln!("warn: session cleanup failed: {e:#}"),
_ => {}
}
}
// cleanup_stale_sentinels=false: server is live, respect active uploads.
let prune_cutoff = std::time::SystemTime::now()
.checked_sub(std::time::Duration::from_secs(24 * 60 * 60))
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
for archive in &cleanup_registry.archives {
if let Ok(paths) = archivr_core::archive::read_archive_paths(&archive.archive_path)
{
let uploads_dir = paths.store_path.join("temp").join("uploads");
prune_stale_upload_dirs(&uploads_dir, prune_cutoff, false);
}
}
tokio::time::sleep(tokio::time::Duration::from_secs(24 * 60 * 60)).await;
}
});
let bind_str = std::env::var("ARCHIVR_BIND")
.ok()
.or_else(|| registry.bind.clone())
.unwrap_or_else(|| DEFAULT_BIND.to_string());
let addr: SocketAddr = bind_str
.parse()
.with_context(|| format!("invalid bind address: {bind_str}"))?;
let listener = tokio::net::TcpListener::bind(addr).await?;
println!("archivr-server listening on http://{addr}");
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}