koprogo_api/infrastructure/storage/
file_storage.rs1use std::fs;
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use uuid::Uuid;
5
6#[derive(Clone)]
8pub struct FileStorage {
9 base_path: PathBuf,
10}
11
12impl FileStorage {
13 pub fn new(base_path: impl AsRef<Path>) -> Result<Self, String> {
15 let base_path = base_path.as_ref().to_path_buf();
16
17 if !base_path.exists() {
19 fs::create_dir_all(&base_path)
20 .map_err(|e| format!("Failed to create storage directory: {}", e))?;
21 }
22
23 Ok(Self { base_path })
24 }
25
26 pub async fn save_file(
29 &self,
30 building_id: Uuid,
31 filename: &str,
32 content: &[u8],
33 ) -> Result<String, String> {
34 let building_dir = self.base_path.join(building_id.to_string());
36 if !building_dir.exists() {
37 fs::create_dir_all(&building_dir)
38 .map_err(|e| format!("Failed to create building directory: {}", e))?;
39 }
40
41 let unique_filename = self.generate_unique_filename(filename);
43 let file_path = building_dir.join(&unique_filename);
44
45 let mut file =
47 fs::File::create(&file_path).map_err(|e| format!("Failed to create file: {}", e))?;
48
49 file.write_all(content)
50 .map_err(|e| format!("Failed to write file: {}", e))?;
51
52 let relative_path = format!("{}/{}", building_id, unique_filename);
54 Ok(relative_path)
55 }
56
57 pub async fn read_file(&self, relative_path: &str) -> Result<Vec<u8>, String> {
59 let file_path = self.base_path.join(relative_path);
60
61 if !file_path.exists() {
62 return Err("File not found".to_string());
63 }
64
65 fs::read(&file_path).map_err(|e| format!("Failed to read file: {}", e))
66 }
67
68 pub async fn delete_file(&self, relative_path: &str) -> Result<(), String> {
70 let file_path = self.base_path.join(relative_path);
71
72 if !file_path.exists() {
73 return Err("File not found".to_string());
74 }
75
76 fs::remove_file(&file_path).map_err(|e| format!("Failed to delete file: {}", e))
77 }
78
79 pub async fn file_exists(&self, relative_path: &str) -> bool {
81 self.base_path.join(relative_path).exists()
82 }
83
84 fn generate_unique_filename(&self, original: &str) -> String {
86 let uuid = Uuid::new_v4();
87 format!("{}_{}", uuid, self.sanitize_filename(original))
88 }
89
90 fn sanitize_filename(&self, filename: &str) -> String {
92 filename.replace("..", "_").replace(['/', '\\'], "_")
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::env;
101
102 #[tokio::test]
103 async fn test_save_and_read_file() {
104 let temp_dir = env::temp_dir().join("koprogo_test_storage");
105 let storage = FileStorage::new(&temp_dir).unwrap();
106
107 let building_id = Uuid::new_v4();
108 let content = b"Test file content";
109
110 let path = storage
111 .save_file(building_id, "test.txt", content)
112 .await
113 .unwrap();
114
115 assert!(storage.file_exists(&path).await);
116
117 let read_content = storage.read_file(&path).await.unwrap();
118 assert_eq!(read_content, content);
119
120 storage.delete_file(&path).await.unwrap();
122 fs::remove_dir_all(&temp_dir).ok();
123 }
124
125 #[tokio::test]
126 async fn test_delete_file() {
127 let temp_dir = env::temp_dir().join("koprogo_test_storage_delete");
128 let storage = FileStorage::new(&temp_dir).unwrap();
129
130 let building_id = Uuid::new_v4();
131 let content = b"Test content";
132
133 let path = storage
134 .save_file(building_id, "delete_me.txt", content)
135 .await
136 .unwrap();
137
138 assert!(storage.file_exists(&path).await);
139
140 storage.delete_file(&path).await.unwrap();
141 assert!(!storage.file_exists(&path).await);
142
143 fs::remove_dir_all(&temp_dir).ok();
145 }
146
147 #[test]
148 fn test_sanitize_filename() {
149 let temp_dir = env::temp_dir().join("koprogo_test");
150 let storage = FileStorage::new(&temp_dir).unwrap();
151
152 assert_eq!(storage.sanitize_filename("../etc/passwd"), "__etc_passwd");
153 assert_eq!(storage.sanitize_filename("normal.pdf"), "normal.pdf");
154
155 fs::remove_dir_all(&temp_dir).ok();
156 }
157}