feat: media API (video/bbox/thumbnail), UUID unification, dot matrix text, portal fixes, API dictionary V1.3

This commit is contained in:
Warren
2026-05-06 13:34:49 +08:00
parent e75c4d6f07
commit 74b6182eba
197 changed files with 17511 additions and 8759 deletions

View File

@@ -1,9 +1,7 @@
pub mod file_manager;
pub mod output_dir;
pub mod snapshot_manager;
pub mod uuid;
pub use file_manager::FileManager;
pub use output_dir::OutputDir;
pub use snapshot_manager::SnapshotManager;
pub use uuid::compute_uuid;

View File

@@ -1,268 +0,0 @@
use std::path::{Path, PathBuf};
use crate::core::config;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SnapshotTier {
Hot,
Warm,
Cold,
}
impl std::fmt::Display for SnapshotTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SnapshotTier::Hot => write!(f, "hot"),
SnapshotTier::Warm => write!(f, "warm"),
SnapshotTier::Cold => write!(f, "cold"),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SnapshotStatus {
pub file_uuid: String,
pub tier: SnapshotTier,
pub hits: u64,
pub types: Vec<String>,
pub generated_at: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SnapshotManager {
base_dir: PathBuf,
}
impl SnapshotManager {
pub fn new(user_dir: &str) -> Self {
let snapshot_dir_name = config::snapshot::SNAPSHOT_DIR_NAME.as_str();
let base_dir = Path::new(user_dir).join(snapshot_dir_name);
Self { base_dir }
}
pub fn base_dir(&self) -> &Path {
&self.base_dir
}
pub fn file_snapshot_dir(&self, file_uuid: &str) -> PathBuf {
self.base_dir.join(file_uuid)
}
pub fn file_type_dir(&self, file_uuid: &str, snapshot_type: &str) -> PathBuf {
self.base_dir.join(file_uuid).join(snapshot_type)
}
pub fn identity_snapshot_dir(&self, identity_uuid: &str) -> PathBuf {
self.base_dir.join("identities").join(identity_uuid)
}
pub fn identity_face_dir(&self, identity_uuid: &str) -> PathBuf {
self.base_dir
.join("identities")
.join(identity_uuid)
.join("faces")
}
pub fn ensure_file_dirs(&self, file_uuid: &str) -> std::io::Result<()> {
let dir = self.file_snapshot_dir(file_uuid);
std::fs::create_dir_all(&dir)?;
for snap_type in ["faces", "logos", "products", "ocr"] {
std::fs::create_dir_all(dir.join(snap_type))?;
}
Ok(())
}
pub fn ensure_identity_dirs(&self, identity_uuid: &str) -> std::io::Result<()> {
let dir = self.identity_snapshot_dir(identity_uuid);
std::fs::create_dir_all(&dir)?;
std::fs::create_dir_all(dir.join("faces"))?;
Ok(())
}
pub fn compute_tier(hits: u64) -> SnapshotTier {
let threshold = *config::snapshot::HOT_THRESHOLD;
if hits >= threshold {
SnapshotTier::Hot
} else if hits > 0 {
SnapshotTier::Warm
} else {
SnapshotTier::Cold
}
}
pub fn tier_ttl(&self, tier: SnapshotTier) -> u64 {
match tier {
SnapshotTier::Hot => *config::snapshot::HOT_TTL_SECS,
SnapshotTier::Warm => *config::snapshot::WARM_TTL_SECS,
SnapshotTier::Cold => 0,
}
}
pub fn snapshot_exists(&self, file_uuid: &str, snapshot_type: &str) -> bool {
self.file_type_dir(file_uuid, snapshot_type).exists()
}
pub fn list_snapshot_types(&self, file_uuid: &str) -> Vec<String> {
let dir = self.file_snapshot_dir(file_uuid);
if !dir.exists() {
return Vec::new();
}
std::fs::read_dir(&dir)
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.filter_map(|e| e.file_name().to_str().map(String::from))
.collect()
}
pub fn remove_file_snapshots(&self, file_uuid: &str) -> std::io::Result<()> {
let dir = self.file_snapshot_dir(file_uuid);
if dir.exists() {
std::fs::remove_dir_all(&dir)?;
}
Ok(())
}
pub fn remove_identity_snapshots(&self, identity_uuid: &str) -> std::io::Result<()> {
let dir = self.identity_snapshot_dir(identity_uuid);
if dir.exists() {
std::fs::remove_dir_all(&dir)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_manager() -> (SnapshotManager, tempfile::TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
let manager = SnapshotManager::new(temp_dir.path().to_str().unwrap());
(manager, temp_dir)
}
#[test]
fn test_compute_tier_hot() {
assert_eq!(SnapshotManager::compute_tier(5), SnapshotTier::Hot);
assert_eq!(SnapshotManager::compute_tier(10), SnapshotTier::Hot);
assert_eq!(SnapshotManager::compute_tier(100), SnapshotTier::Hot);
}
#[test]
fn test_compute_tier_warm() {
assert_eq!(SnapshotManager::compute_tier(1), SnapshotTier::Warm);
assert_eq!(SnapshotManager::compute_tier(4), SnapshotTier::Warm);
}
#[test]
fn test_compute_tier_cold() {
assert_eq!(SnapshotManager::compute_tier(0), SnapshotTier::Cold);
}
#[test]
fn test_tier_display() {
assert_eq!(SnapshotTier::Hot.to_string(), "hot");
assert_eq!(SnapshotTier::Warm.to_string(), "warm");
assert_eq!(SnapshotTier::Cold.to_string(), "cold");
}
#[test]
fn test_ensure_file_dirs_creates_structure() {
let (manager, _temp) = create_test_manager();
let file_uuid = "test_file_123";
manager.ensure_file_dirs(file_uuid).unwrap();
assert!(manager.file_snapshot_dir(file_uuid).exists());
assert!(manager.file_type_dir(file_uuid, "faces").exists());
assert!(manager.file_type_dir(file_uuid, "logos").exists());
assert!(manager.file_type_dir(file_uuid, "products").exists());
assert!(manager.file_type_dir(file_uuid, "ocr").exists());
}
#[test]
fn test_ensure_identity_dirs_creates_structure() {
let (manager, _temp) = create_test_manager();
let identity_uuid = "test_identity_456";
manager.ensure_identity_dirs(identity_uuid).unwrap();
assert!(manager.identity_snapshot_dir(identity_uuid).exists());
assert!(manager.identity_face_dir(identity_uuid).exists());
}
#[test]
fn test_list_snapshot_types_empty() {
let (manager, _temp) = create_test_manager();
let types = manager.list_snapshot_types("nonexistent");
assert!(types.is_empty());
}
#[test]
fn test_list_snapshot_types_after_creation() {
let (manager, _temp) = create_test_manager();
let file_uuid = "test_file_789";
manager.ensure_file_dirs(file_uuid).unwrap();
let types = manager.list_snapshot_types(file_uuid);
assert_eq!(types.len(), 4);
assert!(types.contains(&"faces".to_string()));
assert!(types.contains(&"logos".to_string()));
assert!(types.contains(&"products".to_string()));
assert!(types.contains(&"ocr".to_string()));
}
#[test]
fn test_remove_file_snapshots() {
let (manager, _temp) = create_test_manager();
let file_uuid = "test_file_remove";
manager.ensure_file_dirs(file_uuid).unwrap();
assert!(manager.file_snapshot_dir(file_uuid).exists());
manager.remove_file_snapshots(file_uuid).unwrap();
assert!(!manager.file_snapshot_dir(file_uuid).exists());
}
#[test]
fn test_remove_identity_snapshots() {
let (manager, _temp) = create_test_manager();
let identity_uuid = "test_identity_remove";
manager.ensure_identity_dirs(identity_uuid).unwrap();
assert!(manager.identity_snapshot_dir(identity_uuid).exists());
manager.remove_identity_snapshots(identity_uuid).unwrap();
assert!(!manager.identity_snapshot_dir(identity_uuid).exists());
}
#[test]
fn test_snapshot_exists() {
let (manager, _temp) = create_test_manager();
let file_uuid = "test_exists";
assert!(!manager.snapshot_exists(file_uuid, "faces"));
manager.ensure_file_dirs(file_uuid).unwrap();
assert!(manager.snapshot_exists(file_uuid, "faces"));
assert!(!manager.snapshot_exists(file_uuid, "nonexistent"));
}
#[test]
fn test_tier_ttl() {
let (manager, _temp) = create_test_manager();
let hot_ttl = manager.tier_ttl(SnapshotTier::Hot);
assert_eq!(hot_ttl, *config::snapshot::HOT_TTL_SECS);
let warm_ttl = manager.tier_ttl(SnapshotTier::Warm);
assert_eq!(warm_ttl, *config::snapshot::WARM_TTL_SECS);
let cold_ttl = manager.tier_ttl(SnapshotTier::Cold);
assert_eq!(cold_ttl, 0);
}
}