1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

feat: add web server registry

This commit is contained in:
TheGeneralist 2026-06-01 22:47:00 +02:00
parent 6390853c7b
commit 1634026e5b
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
6 changed files with 753 additions and 6 deletions

View file

@ -0,0 +1,16 @@
[package]
name = "archivr-server"
version.workspace = true
edition.workspace = true
[dependencies]
anyhow.workspace = true
archivr-core = { path = "../archivr-core" }
axum.workspace = true
serde.workspace = true
tokio.workspace = true
toml.workspace = true
tower-http.workspace = true
[dev-dependencies]
tempfile.workspace = true

View file

@ -0,0 +1,20 @@
mod registry;
mod routes;
use anyhow::Result;
use std::{net::SocketAddr, path::PathBuf};
#[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)?;
let app = routes::app(registry);
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
let listener = tokio::net::TcpListener::bind(addr).await?;
println!("archivr-server listening on http://{addr}");
axum::serve(listener, app).await?;
Ok(())
}

View file

@ -0,0 +1,110 @@
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MountedArchive {
pub id: String,
pub label: String,
pub archive_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ServerRegistry {
pub archives: Vec<MountedArchive>,
}
pub fn load_registry(path: &Path) -> Result<ServerRegistry> {
let contents = fs::read_to_string(path)
.with_context(|| format!("failed to read server registry {}", path.display()))?;
let registry = toml::from_str::<ServerRegistry>(&contents)
.with_context(|| format!("failed to parse server registry {}", path.display()))?;
validate_registry(&registry)?;
Ok(registry)
}
pub fn save_registry(path: &Path, registry: &ServerRegistry) -> Result<()> {
validate_registry(registry)?;
let contents = toml::to_string_pretty(registry)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, contents)?;
Ok(())
}
pub fn validate_registry(registry: &ServerRegistry) -> Result<()> {
let mut ids = std::collections::HashSet::new();
for archive in &registry.archives {
if archive.id.trim().is_empty() {
bail!("archive id must not be empty");
}
if !ids.insert(archive.id.as_str()) {
bail!("duplicate archive id: {}", archive.id);
}
if !archive.archive_path.ends_with(".archivr") {
bail!(
"mounted archive path must point at a .archivr directory: {}",
archive.archive_path.display()
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_round_trips_archives_from_toml() {
let temp = tempfile::tempdir().unwrap();
let archive_path = temp.path().join("personal").join(".archivr");
fs::create_dir_all(&archive_path).unwrap();
fs::write(archive_path.join("name"), "Personal").unwrap();
fs::write(
archive_path.join("store_path"),
temp.path().join("store").display().to_string(),
)
.unwrap();
let registry = ServerRegistry {
archives: vec![MountedArchive {
id: "personal".to_string(),
label: "Personal".to_string(),
archive_path: archive_path.clone(),
}],
};
let path = temp.path().join("server.toml");
save_registry(&path, &registry).unwrap();
let loaded = load_registry(&path).unwrap();
assert_eq!(loaded, registry);
}
#[test]
fn registry_rejects_duplicate_archive_ids() {
let registry = ServerRegistry {
archives: vec![
MountedArchive {
id: "personal".to_string(),
label: "Personal".to_string(),
archive_path: PathBuf::from("/tmp/a/.archivr"),
},
MountedArchive {
id: "personal".to_string(),
label: "Duplicate".to_string(),
archive_path: PathBuf::from("/tmp/b/.archivr"),
},
],
};
let err = validate_registry(&registry).unwrap_err().to_string();
assert!(err.contains("duplicate archive id"));
}
}

View file

@ -0,0 +1,7 @@
use axum::{Router, routing::get};
use crate::registry::ServerRegistry;
pub fn app(_registry: ServerRegistry) -> Router {
Router::new().route("/health", get(|| async { "ok" }))
}