Skip to main content

oximedia_proxy/link/
manager.rs

1//! Proxy link manager for managing proxy-original relationships.
2
3use super::database::{LinkDatabase, ProxyLinkRecord};
4use crate::{ProxyError, Result};
5use std::collections::HashMap;
6use std::path::Path;
7
8/// Proxy link manager.
9pub struct ProxyLinkManager {
10    database: LinkDatabase,
11}
12
13impl ProxyLinkManager {
14    /// Create a new proxy link manager with the specified database.
15    ///
16    /// # Errors
17    ///
18    /// Returns an error if the database cannot be opened.
19    pub async fn new(db_path: impl AsRef<Path>) -> Result<Self> {
20        let database = LinkDatabase::new(db_path).await?;
21        Ok(Self { database })
22    }
23
24    /// Link a proxy to its original file.
25    ///
26    /// # Errors
27    ///
28    /// Returns an error if the link cannot be created.
29    pub async fn link_proxy(
30        &mut self,
31        proxy_path: impl AsRef<Path>,
32        original_path: impl AsRef<Path>,
33    ) -> Result<()> {
34        self.link_proxy_with_metadata(
35            proxy_path,
36            original_path,
37            0.25,
38            "h264",
39            0.0,
40            None,
41            HashMap::new(),
42        )
43        .await
44    }
45
46    /// Link a proxy with full metadata.
47    #[allow(clippy::too_many_arguments)]
48    pub async fn link_proxy_with_metadata(
49        &mut self,
50        proxy_path: impl AsRef<Path>,
51        original_path: impl AsRef<Path>,
52        scale_factor: f32,
53        codec: impl Into<String>,
54        duration: f64,
55        timecode: Option<String>,
56        metadata: HashMap<String, String>,
57    ) -> Result<()> {
58        let record = ProxyLinkRecord {
59            proxy_path: proxy_path.as_ref().to_path_buf(),
60            original_path: original_path.as_ref().to_path_buf(),
61            scale_factor,
62            codec: codec.into(),
63            duration,
64            timecode,
65            created_at: current_timestamp(),
66            verified_at: None,
67            metadata,
68        };
69
70        self.database.add_link(record)?;
71
72        tracing::info!(
73            "Linked proxy {} to original {}",
74            proxy_path.as_ref().display(),
75            original_path.as_ref().display()
76        );
77
78        Ok(())
79    }
80
81    /// Get the original path for a proxy.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if no link exists for the proxy.
86    pub fn get_original(&self, proxy_path: impl AsRef<Path>) -> Result<&Path> {
87        self.database
88            .get_link(proxy_path.as_ref())
89            .map(|link| link.original_path.as_path())
90            .ok_or_else(|| ProxyError::LinkNotFound(proxy_path.as_ref().display().to_string()))
91    }
92
93    /// Get the proxy path for an original file.
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if no link exists for the original.
98    pub fn get_proxy(&self, original_path: impl AsRef<Path>) -> Result<&Path> {
99        self.database
100            .get_link_by_original(original_path.as_ref())
101            .map(|link| link.proxy_path.as_path())
102            .ok_or_else(|| ProxyError::LinkNotFound(original_path.as_ref().display().to_string()))
103    }
104
105    /// Check if a proxy link exists.
106    #[must_use]
107    pub fn has_link(&self, proxy_path: impl AsRef<Path>) -> bool {
108        self.database.get_link(proxy_path.as_ref()).is_some()
109    }
110
111    /// Remove a proxy link.
112    pub fn remove_link(&mut self, proxy_path: impl AsRef<Path>) -> Result<()> {
113        self.database.remove_link(proxy_path.as_ref())?;
114        Ok(())
115    }
116
117    /// Verify a proxy link and update its verification timestamp.
118    pub fn verify_link(&mut self, proxy_path: impl AsRef<Path>) -> Result<bool> {
119        let link = self
120            .database
121            .get_link(proxy_path.as_ref())
122            .ok_or_else(|| ProxyError::LinkNotFound(proxy_path.as_ref().display().to_string()))?;
123
124        // Check if both files exist
125        let proxy_exists = link.proxy_path.exists();
126        let original_exists = link.original_path.exists();
127
128        if proxy_exists && original_exists {
129            self.database
130                .update_verification(proxy_path.as_ref(), current_timestamp())?;
131            Ok(true)
132        } else {
133            Ok(false)
134        }
135    }
136
137    /// Get all links in the database.
138    #[must_use]
139    pub fn all_links(&self) -> Vec<ProxyLink> {
140        self.database
141            .all_links()
142            .iter()
143            .map(|record| ProxyLink {
144                proxy_path: record.proxy_path.clone(),
145                original_path: record.original_path.clone(),
146                scale_factor: record.scale_factor,
147                codec: record.codec.clone(),
148                duration: record.duration,
149                timecode: record.timecode.clone(),
150            })
151            .collect()
152    }
153
154    /// Get the number of links in the database.
155    #[must_use]
156    pub fn count(&self) -> usize {
157        self.database.count()
158    }
159}
160
161/// A proxy link (public API).
162#[derive(Debug, Clone)]
163pub struct ProxyLink {
164    /// Proxy file path.
165    pub proxy_path: std::path::PathBuf,
166
167    /// Original file path.
168    pub original_path: std::path::PathBuf,
169
170    /// Proxy resolution scale factor.
171    pub scale_factor: f32,
172
173    /// Proxy codec.
174    pub codec: String,
175
176    /// Duration in seconds.
177    pub duration: f64,
178
179    /// Timecode (if available).
180    pub timecode: Option<String>,
181}
182
183/// Get the current Unix timestamp.
184fn current_timestamp() -> i64 {
185    std::time::SystemTime::now()
186        .duration_since(std::time::UNIX_EPOCH)
187        .expect("infallible: system clock is always after UNIX_EPOCH")
188        .as_secs() as i64
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[tokio::test]
196    async fn test_link_manager() {
197        let temp_dir = std::env::temp_dir();
198        let db_path = temp_dir.join("test_manager.json");
199
200        let mut manager = ProxyLinkManager::new(&db_path)
201            .await
202            .expect("should succeed in test");
203
204        manager
205            .link_proxy("proxy.mp4", "original.mov")
206            .await
207            .expect("should succeed in test");
208
209        let original = manager
210            .get_original("proxy.mp4")
211            .expect("should succeed in test");
212        assert_eq!(original, Path::new("original.mov"));
213
214        assert_eq!(manager.count(), 1);
215
216        // Clean up
217        let _ = std::fs::remove_file(db_path);
218    }
219
220    #[tokio::test]
221    async fn test_has_link() {
222        let temp_dir = std::env::temp_dir();
223        let db_path = temp_dir.join("test_has_link.json");
224
225        let mut manager = ProxyLinkManager::new(&db_path)
226            .await
227            .expect("should succeed in test");
228
229        assert!(!manager.has_link("proxy.mp4"));
230
231        manager
232            .link_proxy("proxy.mp4", "original.mov")
233            .await
234            .expect("should succeed in test");
235
236        assert!(manager.has_link("proxy.mp4"));
237
238        // Clean up
239        let _ = std::fs::remove_file(db_path);
240    }
241}