use super::{StorageError, StorageManager};
use crate::{CoreError, Result};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub base_dir: PathBuf,
pub max_file_size: u64,
pub enable_compression: bool,
pub compression_level: u8,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
base_dir: PathBuf::from("./storage"),
max_file_size: 100 * 1024 * 1024, enable_compression: false,
compression_level: 5,
}
}
}
pub struct FileStorage {
config: StorageConfig,
}
impl FileStorage {
pub fn new(config: StorageConfig) -> Result<Self> {
fs::create_dir_all(&config.base_dir).map_err(|e| CoreError::IoError(e.to_string()))?;
fs::create_dir_all(config.base_dir.join("videos"))
.map_err(|e| CoreError::IoError(e.to_string()))?;
fs::create_dir_all(config.base_dir.join("commentaries"))
.map_err(|e| CoreError::IoError(e.to_string()))?;
fs::create_dir_all(config.base_dir.join("metadata"))
.map_err(|e| CoreError::IoError(e.to_string()))?;
Ok(Self { config })
}
fn get_video_path(&self, id: &str) -> PathBuf {
let base_path = self.config.base_dir.join("videos").join(id);
if self.config.enable_compression {
base_path.with_extension("gz")
} else {
base_path
}
}
fn get_metadata_path(&self, id: &str) -> PathBuf {
let base_path = self
.config
.base_dir
.join("metadata")
.join(format!("{}.json", id));
if self.config.enable_compression {
base_path.with_extension("json.gz")
} else {
base_path
}
}
fn get_commentary_path(&self, id: &str) -> PathBuf {
let base_path = self
.config
.base_dir
.join("commentaries")
.join(format!("{}.json", id));
if self.config.enable_compression {
base_path.with_extension("json.gz")
} else {
base_path
}
}
fn write_file_with_compression(&self, path: &Path, data: &[u8]) -> Result<()> {
if self.config.enable_compression {
let file = File::create(path).map_err(|e| CoreError::IoError(e.to_string()))?;
let compression_level = Compression::new(self.config.compression_level as u32);
let mut encoder = GzEncoder::new(file, compression_level);
encoder
.write_all(data)
.map_err(|e| CoreError::IoError(e.to_string()))?;
encoder
.finish()
.map_err(|e| CoreError::IoError(e.to_string()))?;
} else {
let mut file = File::create(path).map_err(|e| CoreError::IoError(e.to_string()))?;
file.write_all(data)
.map_err(|e| CoreError::IoError(e.to_string()))?;
}
Ok(())
}
fn read_file_with_decompression(&self, path: &Path) -> Result<Vec<u8>> {
if self.config.enable_compression {
let file = File::open(path).map_err(|e| CoreError::IoError(e.to_string()))?;
let mut decoder = GzDecoder::new(file);
let mut data = Vec::new();
decoder
.read_to_end(&mut data)
.map_err(|e| CoreError::IoError(e.to_string()))?;
Ok(data)
} else {
fs::read(path).map_err(|e| CoreError::IoError(e.to_string()))
}
}
}
#[async_trait::async_trait]
impl StorageManager for FileStorage {
async fn store_video(&self, path: &Path, metadata: &serde_json::Value) -> Result<String> {
let file_metadata = fs::metadata(path).map_err(|e| CoreError::IoError(e.to_string()))?;
if file_metadata.len() > self.config.max_file_size {
return Err(CoreError::StorageError(StorageError::FileTooLarge(
file_metadata.len(),
)));
}
let id = Uuid::new_v4().to_string();
let video_content = fs::read(path).map_err(|e| CoreError::IoError(e.to_string()))?;
let video_path = self.get_video_path(&id);
self.write_file_with_compression(&video_path, &video_content)?;
let metadata_path = self.get_metadata_path(&id);
let metadata_json = serde_json::to_string_pretty(metadata)
.map_err(|e| CoreError::JsonError(e.to_string()))?;
self.write_file_with_compression(&metadata_path, metadata_json.as_bytes())?;
Ok(id)
}
async fn retrieve_video(&self, id: &str) -> Result<Vec<u8>> {
let path = self.get_video_path(id);
if !path.exists() {
return Err(CoreError::StorageError(StorageError::FileNotFound(
id.to_string(),
)));
}
self.read_file_with_decompression(&path)
}
async fn get_video_metadata(&self, id: &str) -> Result<serde_json::Value> {
let path = self.get_metadata_path(id);
if !path.exists() {
return Err(CoreError::StorageError(StorageError::FileNotFound(
id.to_string(),
)));
}
let content_bytes = self.read_file_with_decompression(&path)?;
let content =
String::from_utf8(content_bytes).map_err(|e| CoreError::IoError(e.to_string()))?;
Ok(serde_json::from_str(&content).map_err(|e| CoreError::JsonError(e.to_string()))?)
}
async fn delete_video(&self, id: &str) -> Result<bool> {
let video_path = self.get_video_path(id);
let metadata_path = self.get_metadata_path(id);
if video_path.exists() {
fs::remove_file(video_path).map_err(|e| CoreError::IoError(e.to_string()))?;
}
if metadata_path.exists() {
fs::remove_file(metadata_path).map_err(|e| CoreError::IoError(e.to_string()))?;
}
Ok(true)
}
async fn store_commentary(&self, commentary: &serde_json::Value) -> Result<String> {
let id = Uuid::new_v4().to_string();
let path = self.get_commentary_path(&id);
let content = serde_json::to_string_pretty(commentary)
.map_err(|e| CoreError::JsonError(e.to_string()))?;
self.write_file_with_compression(&path, content.as_bytes())?;
Ok(id)
}
async fn retrieve_commentary(&self, id: &str) -> Result<serde_json::Value> {
let path = self.get_commentary_path(id);
if !path.exists() {
return Err(CoreError::StorageError(StorageError::FileNotFound(
id.to_string(),
)));
}
let content_bytes = self.read_file_with_decompression(&path)?;
let content =
String::from_utf8(content_bytes).map_err(|e| CoreError::IoError(e.to_string()))?;
Ok(serde_json::from_str(&content).map_err(|e| CoreError::JsonError(e.to_string()))?)
}
async fn update_commentary(&self, id: &str, commentary: &serde_json::Value) -> Result<bool> {
let path = self.get_commentary_path(id);
if !path.exists() {
return Err(CoreError::StorageError(StorageError::FileNotFound(
id.to_string(),
)));
}
let content = serde_json::to_string_pretty(commentary)
.map_err(|e| CoreError::JsonError(e.to_string()))?;
self.write_file_with_compression(&path, content.as_bytes())?;
Ok(true)
}
async fn delete_commentary(&self, id: &str) -> Result<bool> {
let path = self.get_commentary_path(id);
if !path.exists() {
return Err(CoreError::StorageError(StorageError::FileNotFound(
id.to_string(),
)));
}
fs::remove_file(path).map_err(|e| CoreError::IoError(e.to_string()))?;
Ok(true)
}
async fn list_videos(
&self,
page: u32,
page_size: u32,
) -> Result<Vec<(String, serde_json::Value)>> {
let videos_dir = self.config.base_dir.join("videos");
let mut videos = Vec::new();
for entry in fs::read_dir(videos_dir).map_err(|e| CoreError::IoError(e.to_string()))? {
let entry = entry.map_err(|e| CoreError::IoError(e.to_string()))?;
let file_name = entry.file_name().to_string_lossy().to_string();
let id = if file_name.ends_with(".gz") {
file_name[0..file_name.len() - 3].to_string()
} else {
file_name
};
let metadata_path = self.get_metadata_path(&id);
if metadata_path.exists() {
let content_bytes = self.read_file_with_decompression(&metadata_path)?;
let content = String::from_utf8(content_bytes)
.map_err(|e| CoreError::IoError(e.to_string()))?;
let metadata = serde_json::from_str(&content)
.map_err(|e| CoreError::JsonError(e.to_string()))?;
videos.push((id, metadata));
}
}
let start = (page - 1) as usize * page_size as usize;
let end = start + page_size as usize;
let paginated = videos.into_iter().skip(start).take(end).collect();
Ok(paginated)
}
async fn list_commentaries(&self, video_id: &str) -> Result<Vec<(String, serde_json::Value)>> {
let commentaries_dir = self.config.base_dir.join("commentaries");
let mut commentaries = Vec::new();
for entry in
fs::read_dir(commentaries_dir).map_err(|e| CoreError::IoError(e.to_string()))?
{
let entry = entry.map_err(|e| CoreError::IoError(e.to_string()))?;
let path = entry.path();
let content_bytes = self.read_file_with_decompression(&path)?;
let content =
String::from_utf8(content_bytes).map_err(|e| CoreError::IoError(e.to_string()))?;
let commentary: serde_json::Value =
serde_json::from_str(&content).map_err(|e| CoreError::JsonError(e.to_string()))?;
if let Some(vid) = commentary.get("video_id").and_then(|v| v.as_str()) {
if vid == video_id {
let file_name = entry.file_name().to_string_lossy().to_string();
let id = if file_name.ends_with(".json.gz") {
file_name[0..file_name.len() - 8].to_string()
} else if file_name.ends_with(".json") {
file_name[0..file_name.len() - 5].to_string()
} else {
file_name
};
commentaries.push((id, commentary));
}
}
}
Ok(commentaries)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::tempdir;
#[tokio::test]
async fn test_file_storage_new() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
assert_eq!(storage.config.base_dir, temp_dir.path());
}
#[tokio::test]
async fn test_store_retrieve_video() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
let test_video_path = temp_dir.path().join("test_video.mp4");
std::fs::write(&test_video_path, "test video content").unwrap();
let metadata = json!({"title": "Test Video", "duration": 60});
let video_id = storage
.store_video(&test_video_path, &metadata)
.await
.unwrap();
let retrieved_video = storage.retrieve_video(&video_id).await.unwrap();
assert_eq!(retrieved_video, b"test video content");
let retrieved_metadata = storage.get_video_metadata(&video_id).await.unwrap();
assert_eq!(retrieved_metadata["title"], "Test Video");
assert_eq!(retrieved_metadata["duration"], 60);
}
#[tokio::test]
async fn test_delete_video() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
let test_video_path = temp_dir.path().join("test_video.mp4");
std::fs::write(&test_video_path, "test video content").unwrap();
let metadata = json!({"title": "Test Video", "duration": 60});
let video_id = storage
.store_video(&test_video_path, &metadata)
.await
.unwrap();
let deleted = storage.delete_video(&video_id).await.unwrap();
assert!(deleted);
let result = storage.retrieve_video(&video_id).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_store_retrieve_commentary() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
let commentary = json!({
"video_id": "test-video-id",
"content": "Test commentary",
"style": "professional",
"language": "en"
});
let commentary_id = storage.store_commentary(&commentary).await.unwrap();
let retrieved_commentary = storage.retrieve_commentary(&commentary_id).await.unwrap();
assert_eq!(retrieved_commentary["video_id"], "test-video-id");
assert_eq!(retrieved_commentary["content"], "Test commentary");
}
#[tokio::test]
async fn test_update_commentary() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
let commentary = json!({
"video_id": "test-video-id",
"content": "Test commentary",
"style": "professional",
"language": "en"
});
let commentary_id = storage.store_commentary(&commentary).await.unwrap();
let updated_commentary = json!({
"video_id": "test-video-id",
"content": "Updated commentary",
"style": "professional",
"language": "en"
});
let updated = storage
.update_commentary(&commentary_id, &updated_commentary)
.await
.unwrap();
assert!(updated);
let retrieved_commentary = storage.retrieve_commentary(&commentary_id).await.unwrap();
assert_eq!(retrieved_commentary["content"], "Updated commentary");
}
#[tokio::test]
async fn test_delete_commentary() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
let commentary = json!({
"video_id": "test-video-id",
"content": "Test commentary",
"style": "professional",
"language": "en"
});
let commentary_id = storage.store_commentary(&commentary).await.unwrap();
let deleted = storage.delete_commentary(&commentary_id).await.unwrap();
assert!(deleted);
let result = storage.retrieve_commentary(&commentary_id).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_list_videos() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
for i in 0..3 {
let test_video_path = temp_dir.path().join(format!("test_video_{}.mp4", i));
std::fs::write(&test_video_path, format!("test video content {}", i)).unwrap();
let metadata = json!({
"title": format!("Test Video {}", i),
"duration": 60
});
storage
.store_video(&test_video_path, &metadata)
.await
.unwrap();
}
let videos = storage.list_videos(1, 10).await.unwrap();
assert_eq!(videos.len(), 3);
let videos_page_1 = storage.list_videos(1, 2).await.unwrap();
assert_eq!(videos_page_1.len(), 2);
let videos_page_2 = storage.list_videos(2, 2).await.unwrap();
assert_eq!(videos_page_2.len(), 1);
}
#[tokio::test]
async fn test_list_commentaries() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
for i in 0..3 {
let commentary = json!({
"video_id": "test-video-id",
"content": format!("Test commentary {}", i),
"style": "professional",
"language": "en"
});
storage.store_commentary(&commentary).await.unwrap();
}
let other_commentary = json!({
"video_id": "other-video-id",
"content": "Other video commentary",
"style": "professional",
"language": "en"
});
storage.store_commentary(&other_commentary).await.unwrap();
let commentaries = storage.list_commentaries("test-video-id").await.unwrap();
assert_eq!(commentaries.len(), 3);
let other_commentaries = storage.list_commentaries("other-video-id").await.unwrap();
assert_eq!(other_commentaries.len(), 1);
}
#[tokio::test]
async fn test_file_storage_with_compression() {
let temp_dir = tempdir().unwrap();
let config = StorageConfig {
base_dir: temp_dir.path().to_path_buf(),
enable_compression: true,
..Default::default()
};
let storage = FileStorage::new(config).unwrap();
let test_video_path = temp_dir.path().join("test_video.mp4");
std::fs::write(&test_video_path, "test video content with compression").unwrap();
let metadata = json!({"title": "Test Video", "duration": 60});
let video_id = storage
.store_video(&test_video_path, &metadata)
.await
.unwrap();
let retrieved_video = storage.retrieve_video(&video_id).await.unwrap();
assert_eq!(retrieved_video, b"test video content with compression");
}
}