Skip to main content

oximedia_proxy/link/
database.rs

1//! Proxy link database for storing proxy-original relationships.
2
3use crate::{ProxyError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7
8/// Proxy link database.
9pub struct LinkDatabase {
10    /// Database file path.
11    db_path: PathBuf,
12
13    /// In-memory link storage.
14    links: HashMap<PathBuf, ProxyLinkRecord>,
15}
16
17/// A single proxy link record.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ProxyLinkRecord {
20    /// Proxy file path.
21    pub proxy_path: PathBuf,
22
23    /// Original file path.
24    pub original_path: PathBuf,
25
26    /// Proxy resolution scale factor.
27    pub scale_factor: f32,
28
29    /// Proxy codec.
30    pub codec: String,
31
32    /// Original duration in seconds.
33    pub duration: f64,
34
35    /// Original timecode (if available).
36    pub timecode: Option<String>,
37
38    /// Creation timestamp.
39    pub created_at: i64,
40
41    /// Last verified timestamp.
42    pub verified_at: Option<i64>,
43
44    /// Additional metadata.
45    pub metadata: HashMap<String, String>,
46}
47
48impl LinkDatabase {
49    /// Create or open a link database at the specified path.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if the database cannot be created or opened.
54    pub async fn new(db_path: impl AsRef<Path>) -> Result<Self> {
55        let db_path = db_path.as_ref().to_path_buf();
56
57        // Create parent directory if needed
58        if let Some(parent) = db_path.parent() {
59            std::fs::create_dir_all(parent)?;
60        }
61
62        // Load existing database or create new one
63        let links = if db_path.exists() {
64            Self::load_from_file(&db_path)?
65        } else {
66            HashMap::new()
67        };
68
69        Ok(Self { db_path, links })
70    }
71
72    /// Add a proxy link to the database.
73    pub fn add_link(&mut self, record: ProxyLinkRecord) -> Result<()> {
74        self.links.insert(record.proxy_path.clone(), record);
75        self.save()?;
76        Ok(())
77    }
78
79    /// Get a link by proxy path.
80    #[must_use]
81    pub fn get_link(&self, proxy_path: &Path) -> Option<&ProxyLinkRecord> {
82        self.links.get(proxy_path)
83    }
84
85    /// Get a link by original path.
86    #[must_use]
87    pub fn get_link_by_original(&self, original_path: &Path) -> Option<&ProxyLinkRecord> {
88        self.links
89            .values()
90            .find(|link| link.original_path == original_path)
91    }
92
93    /// Remove a link by proxy path.
94    pub fn remove_link(&mut self, proxy_path: &Path) -> Result<Option<ProxyLinkRecord>> {
95        let result = self.links.remove(proxy_path);
96        self.save()?;
97        Ok(result)
98    }
99
100    /// Update a link's verification timestamp.
101    pub fn update_verification(&mut self, proxy_path: &Path, timestamp: i64) -> Result<()> {
102        if let Some(link) = self.links.get_mut(proxy_path) {
103            link.verified_at = Some(timestamp);
104            self.save()?;
105            Ok(())
106        } else {
107            Err(ProxyError::LinkNotFound(proxy_path.display().to_string()))
108        }
109    }
110
111    /// Get all links in the database.
112    #[must_use]
113    pub fn all_links(&self) -> Vec<&ProxyLinkRecord> {
114        self.links.values().collect()
115    }
116
117    /// Get the number of links in the database.
118    #[must_use]
119    pub fn count(&self) -> usize {
120        self.links.len()
121    }
122
123    /// Save the database to disk.
124    fn save(&self) -> Result<()> {
125        let json = serde_json::to_string_pretty(&self.links)
126            .map_err(|e| ProxyError::DatabaseError(format!("Failed to serialize database: {e}")))?;
127
128        std::fs::write(&self.db_path, json)?;
129        Ok(())
130    }
131
132    /// Load the database from disk.
133    fn load_from_file(path: &Path) -> Result<HashMap<PathBuf, ProxyLinkRecord>> {
134        let content = std::fs::read_to_string(path)?;
135        let links = serde_json::from_str(&content).map_err(|e| {
136            ProxyError::DatabaseError(format!("Failed to deserialize database: {e}"))
137        })?;
138        Ok(links)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[tokio::test]
147    async fn test_database_creation() {
148        let temp_dir = std::env::temp_dir();
149        let db_path = temp_dir.join("test_links.json");
150
151        let db = LinkDatabase::new(&db_path).await;
152        assert!(db.is_ok());
153
154        // Clean up
155        let _ = std::fs::remove_file(db_path);
156    }
157
158    #[tokio::test]
159    async fn test_add_and_get_link() {
160        let temp_dir = std::env::temp_dir();
161        let db_path = temp_dir.join("test_links2.json");
162
163        let mut db = LinkDatabase::new(&db_path)
164            .await
165            .expect("should succeed in test");
166
167        let record = ProxyLinkRecord {
168            proxy_path: PathBuf::from("proxy.mp4"),
169            original_path: PathBuf::from("original.mov"),
170            scale_factor: 0.25,
171            codec: "h264".to_string(),
172            duration: 10.0,
173            timecode: Some("01:00:00:00".to_string()),
174            created_at: 123456789,
175            verified_at: None,
176            metadata: HashMap::new(),
177        };
178
179        db.add_link(record.clone()).expect("should succeed in test");
180
181        let retrieved = db.get_link(Path::new("proxy.mp4"));
182        assert!(retrieved.is_some());
183        assert_eq!(
184            retrieved.expect("should succeed in test").original_path,
185            PathBuf::from("original.mov")
186        );
187
188        // Clean up
189        let _ = std::fs::remove_file(db_path);
190    }
191
192    #[tokio::test]
193    async fn test_remove_link() {
194        let temp_dir = std::env::temp_dir();
195        let db_path = temp_dir.join("test_links3.json");
196
197        let mut db = LinkDatabase::new(&db_path)
198            .await
199            .expect("should succeed in test");
200
201        let record = ProxyLinkRecord {
202            proxy_path: PathBuf::from("proxy.mp4"),
203            original_path: PathBuf::from("original.mov"),
204            scale_factor: 0.25,
205            codec: "h264".to_string(),
206            duration: 10.0,
207            timecode: None,
208            created_at: 123456789,
209            verified_at: None,
210            metadata: HashMap::new(),
211        };
212
213        db.add_link(record).expect("should succeed in test");
214        assert_eq!(db.count(), 1);
215
216        db.remove_link(Path::new("proxy.mp4"))
217            .expect("should succeed in test");
218        assert_eq!(db.count(), 0);
219
220        // Clean up
221        let _ = std::fs::remove_file(db_path);
222    }
223}