1use super::{StorageError, StorageManager};
4use crate::{CoreError, Result};
5use flate2::{read::GzDecoder, write::GzEncoder, Compression};
6use serde::{Deserialize, Serialize};
7use std::fs::{self, File};
8use std::io::{Read, Write};
9use std::path::{Path, PathBuf};
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct StorageConfig {
15 pub base_dir: PathBuf,
17
18 pub max_file_size: u64,
20
21 pub enable_compression: bool,
23
24 pub compression_level: u8,
26}
27
28impl Default for StorageConfig {
29 fn default() -> Self {
30 Self {
31 base_dir: PathBuf::from("./storage"),
32 max_file_size: 100 * 1024 * 1024, enable_compression: false,
34 compression_level: 5,
35 }
36 }
37}
38
39pub struct FileStorage {
41 config: StorageConfig,
42}
43
44impl FileStorage {
45 pub fn new(config: StorageConfig) -> Result<Self> {
47 fs::create_dir_all(&config.base_dir).map_err(|e| CoreError::IoError(e.to_string()))?;
49 fs::create_dir_all(config.base_dir.join("videos"))
50 .map_err(|e| CoreError::IoError(e.to_string()))?;
51 fs::create_dir_all(config.base_dir.join("commentaries"))
52 .map_err(|e| CoreError::IoError(e.to_string()))?;
53 fs::create_dir_all(config.base_dir.join("metadata"))
54 .map_err(|e| CoreError::IoError(e.to_string()))?;
55
56 Ok(Self { config })
57 }
58
59 fn get_video_path(&self, id: &str) -> PathBuf {
61 let base_path = self.config.base_dir.join("videos").join(id);
62 if self.config.enable_compression {
63 base_path.with_extension("gz")
64 } else {
65 base_path
66 }
67 }
68
69 fn get_metadata_path(&self, id: &str) -> PathBuf {
71 let base_path = self
72 .config
73 .base_dir
74 .join("metadata")
75 .join(format!("{}.json", id));
76 if self.config.enable_compression {
77 base_path.with_extension("json.gz")
78 } else {
79 base_path
80 }
81 }
82
83 fn get_commentary_path(&self, id: &str) -> PathBuf {
85 let base_path = self
86 .config
87 .base_dir
88 .join("commentaries")
89 .join(format!("{}.json", id));
90 if self.config.enable_compression {
91 base_path.with_extension("json.gz")
92 } else {
93 base_path
94 }
95 }
96
97 fn write_file_with_compression(&self, path: &Path, data: &[u8]) -> Result<()> {
99 if self.config.enable_compression {
100 let file = File::create(path).map_err(|e| CoreError::IoError(e.to_string()))?;
101 let compression_level = Compression::new(self.config.compression_level as u32);
102 let mut encoder = GzEncoder::new(file, compression_level);
103 encoder
104 .write_all(data)
105 .map_err(|e| CoreError::IoError(e.to_string()))?;
106 encoder
107 .finish()
108 .map_err(|e| CoreError::IoError(e.to_string()))?;
109 } else {
110 let mut file = File::create(path).map_err(|e| CoreError::IoError(e.to_string()))?;
111 file.write_all(data)
112 .map_err(|e| CoreError::IoError(e.to_string()))?;
113 }
114 Ok(())
115 }
116
117 fn read_file_with_decompression(&self, path: &Path) -> Result<Vec<u8>> {
119 if self.config.enable_compression {
120 let file = File::open(path).map_err(|e| CoreError::IoError(e.to_string()))?;
121 let mut decoder = GzDecoder::new(file);
122 let mut data = Vec::new();
123 decoder
124 .read_to_end(&mut data)
125 .map_err(|e| CoreError::IoError(e.to_string()))?;
126 Ok(data)
127 } else {
128 fs::read(path).map_err(|e| CoreError::IoError(e.to_string()))
129 }
130 }
131}
132
133#[async_trait::async_trait]
134impl StorageManager for FileStorage {
135 async fn store_video(&self, path: &Path, metadata: &serde_json::Value) -> Result<String> {
136 let file_metadata = fs::metadata(path).map_err(|e| CoreError::IoError(e.to_string()))?;
138 if file_metadata.len() > self.config.max_file_size {
139 return Err(CoreError::StorageError(StorageError::FileTooLarge(
140 file_metadata.len(),
141 )));
142 }
143
144 let id = Uuid::new_v4().to_string();
146
147 let video_content = fs::read(path).map_err(|e| CoreError::IoError(e.to_string()))?;
149
150 let video_path = self.get_video_path(&id);
152 self.write_file_with_compression(&video_path, &video_content)?;
153
154 let metadata_path = self.get_metadata_path(&id);
156 let metadata_json = serde_json::to_string_pretty(metadata)
157 .map_err(|e| CoreError::JsonError(e.to_string()))?;
158 self.write_file_with_compression(&metadata_path, metadata_json.as_bytes())?;
159
160 Ok(id)
161 }
162
163 async fn retrieve_video(&self, id: &str) -> Result<Vec<u8>> {
164 let path = self.get_video_path(id);
165 if !path.exists() {
166 return Err(CoreError::StorageError(StorageError::FileNotFound(
167 id.to_string(),
168 )));
169 }
170
171 self.read_file_with_decompression(&path)
172 }
173
174 async fn get_video_metadata(&self, id: &str) -> Result<serde_json::Value> {
175 let path = self.get_metadata_path(id);
176 if !path.exists() {
177 return Err(CoreError::StorageError(StorageError::FileNotFound(
178 id.to_string(),
179 )));
180 }
181
182 let content_bytes = self.read_file_with_decompression(&path)?;
183 let content =
184 String::from_utf8(content_bytes).map_err(|e| CoreError::IoError(e.to_string()))?;
185 Ok(serde_json::from_str(&content).map_err(|e| CoreError::JsonError(e.to_string()))?)
186 }
187
188 async fn delete_video(&self, id: &str) -> Result<bool> {
189 let video_path = self.get_video_path(id);
190 let metadata_path = self.get_metadata_path(id);
191
192 if video_path.exists() {
194 fs::remove_file(video_path).map_err(|e| CoreError::IoError(e.to_string()))?;
195 }
196
197 if metadata_path.exists() {
199 fs::remove_file(metadata_path).map_err(|e| CoreError::IoError(e.to_string()))?;
200 }
201
202 Ok(true)
203 }
204
205 async fn store_commentary(&self, commentary: &serde_json::Value) -> Result<String> {
206 let id = Uuid::new_v4().to_string();
208
209 let path = self.get_commentary_path(&id);
211 let content = serde_json::to_string_pretty(commentary)
212 .map_err(|e| CoreError::JsonError(e.to_string()))?;
213 self.write_file_with_compression(&path, content.as_bytes())?;
214
215 Ok(id)
216 }
217
218 async fn retrieve_commentary(&self, id: &str) -> Result<serde_json::Value> {
219 let path = self.get_commentary_path(id);
220 if !path.exists() {
221 return Err(CoreError::StorageError(StorageError::FileNotFound(
222 id.to_string(),
223 )));
224 }
225
226 let content_bytes = self.read_file_with_decompression(&path)?;
227 let content =
228 String::from_utf8(content_bytes).map_err(|e| CoreError::IoError(e.to_string()))?;
229 Ok(serde_json::from_str(&content).map_err(|e| CoreError::JsonError(e.to_string()))?)
230 }
231
232 async fn update_commentary(&self, id: &str, commentary: &serde_json::Value) -> Result<bool> {
233 let path = self.get_commentary_path(id);
234 if !path.exists() {
235 return Err(CoreError::StorageError(StorageError::FileNotFound(
236 id.to_string(),
237 )));
238 }
239
240 let content = serde_json::to_string_pretty(commentary)
242 .map_err(|e| CoreError::JsonError(e.to_string()))?;
243 self.write_file_with_compression(&path, content.as_bytes())?;
244
245 Ok(true)
246 }
247
248 async fn delete_commentary(&self, id: &str) -> Result<bool> {
249 let path = self.get_commentary_path(id);
250 if !path.exists() {
251 return Err(CoreError::StorageError(StorageError::FileNotFound(
252 id.to_string(),
253 )));
254 }
255
256 fs::remove_file(path).map_err(|e| CoreError::IoError(e.to_string()))?;
257 Ok(true)
258 }
259
260 async fn list_videos(
261 &self,
262 page: u32,
263 page_size: u32,
264 ) -> Result<Vec<(String, serde_json::Value)>> {
265 let videos_dir = self.config.base_dir.join("videos");
266
267 let mut videos = Vec::new();
269 for entry in fs::read_dir(videos_dir).map_err(|e| CoreError::IoError(e.to_string()))? {
270 let entry = entry.map_err(|e| CoreError::IoError(e.to_string()))?;
271 let file_name = entry.file_name().to_string_lossy().to_string();
272
273 let id = if file_name.ends_with(".gz") {
275 file_name[0..file_name.len() - 3].to_string()
276 } else {
277 file_name
278 };
279
280 let metadata_path = self.get_metadata_path(&id);
282 if metadata_path.exists() {
283 let content_bytes = self.read_file_with_decompression(&metadata_path)?;
285 let content = String::from_utf8(content_bytes)
286 .map_err(|e| CoreError::IoError(e.to_string()))?;
287 let metadata = serde_json::from_str(&content)
288 .map_err(|e| CoreError::JsonError(e.to_string()))?;
289 videos.push((id, metadata));
290 }
291 }
292
293 let start = (page - 1) as usize * page_size as usize;
295 let end = start + page_size as usize;
296 let paginated = videos.into_iter().skip(start).take(end).collect();
297
298 Ok(paginated)
299 }
300
301 async fn list_commentaries(&self, video_id: &str) -> Result<Vec<(String, serde_json::Value)>> {
302 let commentaries_dir = self.config.base_dir.join("commentaries");
303
304 let mut commentaries = Vec::new();
306 for entry in
307 fs::read_dir(commentaries_dir).map_err(|e| CoreError::IoError(e.to_string()))?
308 {
309 let entry = entry.map_err(|e| CoreError::IoError(e.to_string()))?;
310 let path = entry.path();
311
312 let content_bytes = self.read_file_with_decompression(&path)?;
314 let content =
315 String::from_utf8(content_bytes).map_err(|e| CoreError::IoError(e.to_string()))?;
316 let commentary: serde_json::Value =
317 serde_json::from_str(&content).map_err(|e| CoreError::JsonError(e.to_string()))?;
318
319 if let Some(vid) = commentary.get("video_id").and_then(|v| v.as_str()) {
321 if vid == video_id {
322 let file_name = entry.file_name().to_string_lossy().to_string();
323 let id = if file_name.ends_with(".json.gz") {
325 file_name[0..file_name.len() - 8].to_string()
326 } else if file_name.ends_with(".json") {
327 file_name[0..file_name.len() - 5].to_string()
328 } else {
329 file_name
330 };
331 commentaries.push((id, commentary));
332 }
333 }
334 }
335
336 Ok(commentaries)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use serde_json::json;
344 use tempfile::tempdir;
345
346 #[tokio::test]
347 async fn test_file_storage_new() {
348 let temp_dir = tempdir().unwrap();
349 let config = StorageConfig {
350 base_dir: temp_dir.path().to_path_buf(),
351 ..Default::default()
352 };
353
354 let storage = FileStorage::new(config).unwrap();
355 assert_eq!(storage.config.base_dir, temp_dir.path());
356 }
357
358 #[tokio::test]
359 async fn test_store_retrieve_video() {
360 let temp_dir = tempdir().unwrap();
361 let config = StorageConfig {
362 base_dir: temp_dir.path().to_path_buf(),
363 ..Default::default()
364 };
365
366 let storage = FileStorage::new(config).unwrap();
367
368 let test_video_path = temp_dir.path().join("test_video.mp4");
370 std::fs::write(&test_video_path, "test video content").unwrap();
371
372 let metadata = json!({"title": "Test Video", "duration": 60});
374 let video_id = storage
375 .store_video(&test_video_path, &metadata)
376 .await
377 .unwrap();
378
379 let retrieved_video = storage.retrieve_video(&video_id).await.unwrap();
381 assert_eq!(retrieved_video, b"test video content");
382
383 let retrieved_metadata = storage.get_video_metadata(&video_id).await.unwrap();
385 assert_eq!(retrieved_metadata["title"], "Test Video");
386 assert_eq!(retrieved_metadata["duration"], 60);
387 }
388
389 #[tokio::test]
390 async fn test_delete_video() {
391 let temp_dir = tempdir().unwrap();
392 let config = StorageConfig {
393 base_dir: temp_dir.path().to_path_buf(),
394 ..Default::default()
395 };
396
397 let storage = FileStorage::new(config).unwrap();
398
399 let test_video_path = temp_dir.path().join("test_video.mp4");
401 std::fs::write(&test_video_path, "test video content").unwrap();
402
403 let metadata = json!({"title": "Test Video", "duration": 60});
405 let video_id = storage
406 .store_video(&test_video_path, &metadata)
407 .await
408 .unwrap();
409
410 let deleted = storage.delete_video(&video_id).await.unwrap();
412 assert!(deleted);
413
414 let result = storage.retrieve_video(&video_id).await;
416 assert!(result.is_err());
417 }
418
419 #[tokio::test]
420 async fn test_store_retrieve_commentary() {
421 let temp_dir = tempdir().unwrap();
422 let config = StorageConfig {
423 base_dir: temp_dir.path().to_path_buf(),
424 ..Default::default()
425 };
426
427 let storage = FileStorage::new(config).unwrap();
428
429 let commentary = json!({
431 "video_id": "test-video-id",
432 "content": "Test commentary",
433 "style": "professional",
434 "language": "en"
435 });
436 let commentary_id = storage.store_commentary(&commentary).await.unwrap();
437
438 let retrieved_commentary = storage.retrieve_commentary(&commentary_id).await.unwrap();
440 assert_eq!(retrieved_commentary["video_id"], "test-video-id");
441 assert_eq!(retrieved_commentary["content"], "Test commentary");
442 }
443
444 #[tokio::test]
445 async fn test_update_commentary() {
446 let temp_dir = tempdir().unwrap();
447 let config = StorageConfig {
448 base_dir: temp_dir.path().to_path_buf(),
449 ..Default::default()
450 };
451
452 let storage = FileStorage::new(config).unwrap();
453
454 let commentary = json!({
456 "video_id": "test-video-id",
457 "content": "Test commentary",
458 "style": "professional",
459 "language": "en"
460 });
461 let commentary_id = storage.store_commentary(&commentary).await.unwrap();
462
463 let updated_commentary = json!({
465 "video_id": "test-video-id",
466 "content": "Updated commentary",
467 "style": "professional",
468 "language": "en"
469 });
470 let updated = storage
471 .update_commentary(&commentary_id, &updated_commentary)
472 .await
473 .unwrap();
474 assert!(updated);
475
476 let retrieved_commentary = storage.retrieve_commentary(&commentary_id).await.unwrap();
478 assert_eq!(retrieved_commentary["content"], "Updated commentary");
479 }
480
481 #[tokio::test]
482 async fn test_delete_commentary() {
483 let temp_dir = tempdir().unwrap();
484 let config = StorageConfig {
485 base_dir: temp_dir.path().to_path_buf(),
486 ..Default::default()
487 };
488
489 let storage = FileStorage::new(config).unwrap();
490
491 let commentary = json!({
493 "video_id": "test-video-id",
494 "content": "Test commentary",
495 "style": "professional",
496 "language": "en"
497 });
498 let commentary_id = storage.store_commentary(&commentary).await.unwrap();
499
500 let deleted = storage.delete_commentary(&commentary_id).await.unwrap();
502 assert!(deleted);
503
504 let result = storage.retrieve_commentary(&commentary_id).await;
506 assert!(result.is_err());
507 }
508
509 #[tokio::test]
510 async fn test_list_videos() {
511 let temp_dir = tempdir().unwrap();
512 let config = StorageConfig {
513 base_dir: temp_dir.path().to_path_buf(),
514 ..Default::default()
515 };
516
517 let storage = FileStorage::new(config).unwrap();
518
519 for i in 0..3 {
521 let test_video_path = temp_dir.path().join(format!("test_video_{}.mp4", i));
522 std::fs::write(&test_video_path, format!("test video content {}", i)).unwrap();
523
524 let metadata = json!({
525 "title": format!("Test Video {}", i),
526 "duration": 60
527 });
528 storage
529 .store_video(&test_video_path, &metadata)
530 .await
531 .unwrap();
532 }
533
534 let videos = storage.list_videos(1, 10).await.unwrap();
536 assert_eq!(videos.len(), 3);
537
538 let videos_page_1 = storage.list_videos(1, 2).await.unwrap();
540 assert_eq!(videos_page_1.len(), 2);
541
542 let videos_page_2 = storage.list_videos(2, 2).await.unwrap();
543 assert_eq!(videos_page_2.len(), 1);
544 }
545
546 #[tokio::test]
547 async fn test_list_commentaries() {
548 let temp_dir = tempdir().unwrap();
549 let config = StorageConfig {
550 base_dir: temp_dir.path().to_path_buf(),
551 ..Default::default()
552 };
553
554 let storage = FileStorage::new(config).unwrap();
555
556 for i in 0..3 {
558 let commentary = json!({
559 "video_id": "test-video-id",
560 "content": format!("Test commentary {}", i),
561 "style": "professional",
562 "language": "en"
563 });
564 storage.store_commentary(&commentary).await.unwrap();
565 }
566
567 let other_commentary = json!({
569 "video_id": "other-video-id",
570 "content": "Other video commentary",
571 "style": "professional",
572 "language": "en"
573 });
574 storage.store_commentary(&other_commentary).await.unwrap();
575
576 let commentaries = storage.list_commentaries("test-video-id").await.unwrap();
578 assert_eq!(commentaries.len(), 3);
579
580 let other_commentaries = storage.list_commentaries("other-video-id").await.unwrap();
582 assert_eq!(other_commentaries.len(), 1);
583 }
584
585 #[tokio::test]
586 async fn test_file_storage_with_compression() {
587 let temp_dir = tempdir().unwrap();
588 let config = StorageConfig {
589 base_dir: temp_dir.path().to_path_buf(),
590 enable_compression: true,
591 ..Default::default()
592 };
593
594 let storage = FileStorage::new(config).unwrap();
595
596 let test_video_path = temp_dir.path().join("test_video.mp4");
598 std::fs::write(&test_video_path, "test video content with compression").unwrap();
599
600 let metadata = json!({"title": "Test Video", "duration": 60});
602 let video_id = storage
603 .store_video(&test_video_path, &metadata)
604 .await
605 .unwrap();
606
607 let retrieved_video = storage.retrieve_video(&video_id).await.unwrap();
609 assert_eq!(retrieved_video, b"test video content with compression");
610 }
611}