koprogo_api/infrastructure/storage/
file_storage.rs1use super::{metrics::record_storage_operation, StorageProvider};
2use async_trait::async_trait;
3use std::fs;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::time::Instant;
7use uuid::Uuid;
8
9#[derive(Clone)]
11pub struct FileStorage {
12 base_path: PathBuf,
13}
14
15impl FileStorage {
16 pub fn new(base_path: impl AsRef<Path>) -> Result<Self, String> {
18 let base_path = base_path.as_ref().to_path_buf();
19
20 if !base_path.exists() {
22 fs::create_dir_all(&base_path)
23 .map_err(|e| format!("Failed to create storage directory: {}", e))?;
24 }
25
26 Ok(Self { base_path })
27 }
28
29 pub async fn save_file(
32 &self,
33 building_id: Uuid,
34 filename: &str,
35 content: &[u8],
36 ) -> Result<String, String> {
37 let building_dir = self.base_path.join(building_id.to_string());
39 if !building_dir.exists() {
40 fs::create_dir_all(&building_dir)
41 .map_err(|e| format!("Failed to create building directory: {}", e))?;
42 }
43
44 let unique_filename = self.generate_unique_filename(filename);
46 let file_path = building_dir.join(&unique_filename);
47
48 let mut file =
50 fs::File::create(&file_path).map_err(|e| format!("Failed to create file: {}", e))?;
51
52 file.write_all(content)
53 .map_err(|e| format!("Failed to write file: {}", e))?;
54
55 let relative_path = format!("{}/{}", building_id, unique_filename);
57 Ok(relative_path)
58 }
59
60 pub async fn read_file(&self, relative_path: &str) -> Result<Vec<u8>, String> {
62 let file_path = self.base_path.join(relative_path);
63
64 if !file_path.exists() {
65 return Err("File not found".to_string());
66 }
67
68 fs::read(&file_path).map_err(|e| format!("Failed to read file: {}", e))
69 }
70
71 pub async fn delete_file(&self, relative_path: &str) -> Result<(), String> {
73 let file_path = self.base_path.join(relative_path);
74
75 if !file_path.exists() {
76 return Err("File not found".to_string());
77 }
78
79 fs::remove_file(&file_path).map_err(|e| format!("Failed to delete file: {}", e))
80 }
81
82 pub async fn file_exists(&self, relative_path: &str) -> bool {
84 self.base_path.join(relative_path).exists()
85 }
86
87 fn generate_unique_filename(&self, original: &str) -> String {
89 let uuid = Uuid::new_v4();
90 format!("{}_{}", uuid, self.sanitize_filename(original))
91 }
92
93 fn sanitize_filename(&self, filename: &str) -> String {
95 filename.replace("..", "_").replace(['/', '\\'], "_")
97 }
98}
99
100#[async_trait]
101impl StorageProvider for FileStorage {
102 async fn save_file(
103 &self,
104 building_id: Uuid,
105 filename: &str,
106 content: &[u8],
107 ) -> Result<String, String> {
108 let start = Instant::now();
109 let result = FileStorage::save_file(self, building_id, filename, content).await;
110 record_storage_operation(
111 "local",
112 "save_file",
113 start.elapsed(),
114 result.as_ref().map(|_| ()).map_err(|e| e.as_str()),
115 );
116 result
117 }
118
119 async fn read_file(&self, relative_path: &str) -> Result<Vec<u8>, String> {
120 let start = Instant::now();
121 let result = FileStorage::read_file(self, relative_path).await;
122 record_storage_operation(
123 "local",
124 "read_file",
125 start.elapsed(),
126 result.as_ref().map(|_| ()).map_err(|e| e.as_str()),
127 );
128 result
129 }
130
131 async fn delete_file(&self, relative_path: &str) -> Result<(), String> {
132 let start = Instant::now();
133 let result = FileStorage::delete_file(self, relative_path).await;
134 record_storage_operation(
135 "local",
136 "delete_file",
137 start.elapsed(),
138 result.as_ref().map(|_| ()).map_err(|e| e.as_str()),
139 );
140 result
141 }
142
143 async fn file_exists(&self, relative_path: &str) -> bool {
144 let start = Instant::now();
145 let exists = FileStorage::file_exists(self, relative_path).await;
146 record_storage_operation("local", "file_exists", start.elapsed(), Ok(()));
147 exists
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use std::env;
155
156 #[tokio::test]
157 async fn test_save_and_read_file() {
158 let temp_dir = env::temp_dir().join("koprogo_test_storage");
159 let storage = FileStorage::new(&temp_dir).unwrap();
160
161 let building_id = Uuid::new_v4();
162 let content = b"Test file content";
163
164 let path = storage
165 .save_file(building_id, "test.txt", content)
166 .await
167 .unwrap();
168
169 assert!(storage.file_exists(&path).await);
170
171 let read_content = storage.read_file(&path).await.unwrap();
172 assert_eq!(read_content, content);
173
174 storage.delete_file(&path).await.unwrap();
176 fs::remove_dir_all(&temp_dir).ok();
177 }
178
179 #[tokio::test]
180 async fn test_delete_file() {
181 let temp_dir = env::temp_dir().join("koprogo_test_storage_delete");
182 let storage = FileStorage::new(&temp_dir).unwrap();
183
184 let building_id = Uuid::new_v4();
185 let content = b"Test content";
186
187 let path = storage
188 .save_file(building_id, "delete_me.txt", content)
189 .await
190 .unwrap();
191
192 assert!(storage.file_exists(&path).await);
193
194 storage.delete_file(&path).await.unwrap();
195 assert!(!storage.file_exists(&path).await);
196
197 fs::remove_dir_all(&temp_dir).ok();
199 }
200
201 #[test]
202 fn test_sanitize_filename() {
203 let temp_dir = env::temp_dir().join("koprogo_test");
204 let storage = FileStorage::new(&temp_dir).unwrap();
205
206 assert_eq!(storage.sanitize_filename("../etc/passwd"), "__etc_passwd");
207 assert_eq!(storage.sanitize_filename("normal.pdf"), "normal.pdf");
208
209 fs::remove_dir_all(&temp_dir).ok();
210 }
211}