oximedia_proxy/link/
manager.rs1use super::database::{LinkDatabase, ProxyLinkRecord};
4use crate::{ProxyError, Result};
5use std::collections::HashMap;
6use std::path::Path;
7
8pub struct ProxyLinkManager {
10 database: LinkDatabase,
11}
12
13impl ProxyLinkManager {
14 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 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 #[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 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 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 #[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 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 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 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 #[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 #[must_use]
156 pub fn count(&self) -> usize {
157 self.database.count()
158 }
159}
160
161#[derive(Debug, Clone)]
163pub struct ProxyLink {
164 pub proxy_path: std::path::PathBuf,
166
167 pub original_path: std::path::PathBuf,
169
170 pub scale_factor: f32,
172
173 pub codec: String,
175
176 pub duration: f64,
178
179 pub timecode: Option<String>,
181}
182
183fn 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 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 let _ = std::fs::remove_file(db_path);
240 }
241}