mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-22 03:05:32 +02:00
feat: add db and multi-archive web UI foundation (#8)
* Add SQLite metadata database support * Implement archive metadata database * chore: let's guess cargoHash because there's something wrong with nixpkgs! * Gate test-only database helpers behind cfg(test) * Fix archive database row identity * Use serde for archive metadata JSON * Finalize archive runs at command level * Handle archive command errors without panics * Cover tweet entry metadata recording * Document static regex invariants * docs: add web UI design spec * docs: add web UI implementation plan * chore: move cli into workspace crate * chore: track workspace crates directory * refactor: extract archive core crate * refactor: add core archive opening APIs * refactor: rename taxonomy model to tags * feat: add archive query APIs * feat: add web server registry * feat: expose archive server APIs * feat: add archive table web UI * fix: complete web UI smoke path * docs: add architecture mental model * docs: remove private superpowers plans * nix: split cli and server packages * chore: remove PLAN.md
This commit is contained in:
parent
cc380ec5ba
commit
b56c969624
25 changed files with 3928 additions and 171 deletions
111
crates/archivr-server/src/registry.rs
Normal file
111
crates/archivr-server/src/registry.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
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(®istry)?;
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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 ®istry.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, ®istry).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(®istry).unwrap_err().to_string();
|
||||
|
||||
assert!(err.contains("duplicate archive id"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue