Ny crate `tools/synops-common` samler duplisert kode som var
spredt over 13 CLI-verktøy:
- db::connect() — PG-pool fra DATABASE_URL (erstatter 10+ identiske blokker)
- cas::path() — CAS-stioppslag med to-nivå hash-katalog
- cas::root() — CAS_ROOT env med default
- cas::hash_bytes() / hash_file() / store() — SHA-256 hashing og lagring
- cas::mime_to_extension() — MIME → filendelse
- logging::init() — tracing til stderr med env-filter
- types::{NodeRow, EdgeRow, NodeSummary} — delte FromRow-structs
Alle verktøy (unntatt synops-tasks som ikke bruker DB) er refaktorert
til å bruke synops-common. Alle kompilerer og tester passerer.
127 lines
3.6 KiB
Rust
127 lines
3.6 KiB
Rust
//! Content-Addressable Store (CAS) utilities.
|
|
//!
|
|
//! CAS lagrer filer med SHA-256-hash som nøkkel, organisert i
|
|
//! en to-nivå katalogstruktur: `{root}/{hash[0..2]}/{hash[2..4]}/{hash}`.
|
|
//!
|
|
//! Miljøvariabel: CAS_ROOT (default: /srv/synops/media/cas)
|
|
|
|
use sha2::{Digest, Sha256};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Standard CAS-rotkatalog.
|
|
pub const DEFAULT_CAS_ROOT: &str = "/srv/synops/media/cas";
|
|
|
|
/// Hent CAS-rotkatalog fra miljøvariabel eller bruk default.
|
|
pub fn root() -> String {
|
|
std::env::var("CAS_ROOT").unwrap_or_else(|_| DEFAULT_CAS_ROOT.into())
|
|
}
|
|
|
|
/// Beregn filsti i CAS fra rot og hash.
|
|
///
|
|
/// Struktur: `{root}/{hash[0..2]}/{hash[2..4]}/{hash}`
|
|
///
|
|
/// # Eksempel
|
|
/// ```
|
|
/// use synops_common::cas;
|
|
/// let p = cas::path("/srv/synops/media/cas", "b94d27b9934d3e08");
|
|
/// assert_eq!(p.to_str().unwrap(), "/srv/synops/media/cas/b9/4d/b94d27b9934d3e08");
|
|
/// ```
|
|
pub fn path(root: &str, hash: &str) -> PathBuf {
|
|
let (p1, rest) = hash.split_at(2.min(hash.len()));
|
|
let (p2, _) = rest.split_at(2.min(rest.len()));
|
|
PathBuf::from(root).join(p1).join(p2).join(hash)
|
|
}
|
|
|
|
/// Beregn SHA-256 hash av bytes. Returnerer hex-streng.
|
|
pub fn hash_bytes(data: &[u8]) -> String {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(data);
|
|
hex::encode(hasher.finalize())
|
|
}
|
|
|
|
/// Beregn SHA-256 hash av en fil. Returnerer hex-streng.
|
|
pub async fn hash_file(file_path: &Path) -> Result<String, String> {
|
|
let data = tokio::fs::read(file_path)
|
|
.await
|
|
.map_err(|e| format!("Kunne ikke lese fil {}: {e}", file_path.display()))?;
|
|
Ok(hash_bytes(&data))
|
|
}
|
|
|
|
/// Lagre bytes i CAS. Returnerer hash. Oppretter kataloger ved behov.
|
|
pub async fn store(cas_root: &str, data: &[u8]) -> Result<String, String> {
|
|
let hash = hash_bytes(data);
|
|
let target = path(cas_root, &hash);
|
|
|
|
if target.exists() {
|
|
return Ok(hash);
|
|
}
|
|
|
|
if let Some(parent) = target.parent() {
|
|
tokio::fs::create_dir_all(parent)
|
|
.await
|
|
.map_err(|e| format!("Kunne ikke opprette CAS-katalog: {e}"))?;
|
|
}
|
|
|
|
tokio::fs::write(&target, data)
|
|
.await
|
|
.map_err(|e| format!("Kunne ikke skrive CAS-fil: {e}"))?;
|
|
|
|
Ok(hash)
|
|
}
|
|
|
|
/// Konverter MIME-type til filendelse.
|
|
pub fn mime_to_extension(mime: &str) -> &str {
|
|
match mime {
|
|
"audio/mpeg" | "audio/mp3" => "mp3",
|
|
"audio/wav" | "audio/x-wav" => "wav",
|
|
"audio/ogg" => "ogg",
|
|
"audio/flac" | "audio/x-flac" => "flac",
|
|
"audio/mp4" | "audio/m4a" | "audio/x-m4a" => "m4a",
|
|
"audio/webm" => "webm",
|
|
"video/mp4" => "mp4",
|
|
"video/webm" => "webm",
|
|
"image/jpeg" => "jpg",
|
|
"image/png" => "png",
|
|
"image/webp" => "webp",
|
|
"image/gif" => "gif",
|
|
"text/html" => "html",
|
|
"application/pdf" => "pdf",
|
|
_ => "bin",
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn cas_path_format() {
|
|
let p = path("/srv/synops/media/cas", "b94d27b9934d3e08");
|
|
assert_eq!(
|
|
p,
|
|
PathBuf::from("/srv/synops/media/cas/b9/4d/b94d27b9934d3e08")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cas_path_short_hash() {
|
|
let p = path("/cas", "ab");
|
|
assert_eq!(p, PathBuf::from("/cas/ab//ab"));
|
|
}
|
|
|
|
#[test]
|
|
fn hash_known_value() {
|
|
let hash = hash_bytes(b"hello world");
|
|
assert_eq!(
|
|
hash,
|
|
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mime_mapping() {
|
|
assert_eq!(mime_to_extension("audio/mpeg"), "mp3");
|
|
assert_eq!(mime_to_extension("image/png"), "png");
|
|
assert_eq!(mime_to_extension("unknown/type"), "bin");
|
|
}
|
|
}
|