1#![allow(dead_code)]
4#![allow(unused_variables)]
5#![allow(unused_imports)]
6
7use async_trait::async_trait;
8use bytes::Bytes;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use chrono::DateTime;
12use tracing::info;
13
14use crate::{
15 StorageResult, StorageError, StorageService, StorageConfig, StorageFile,
16 StorageStats, UploadOptions, DownloadOptions, PresignedUrlOptions,
17 ByteRange, detect_mime_type, sanitize_filename,
18 LocalStorageConfig,
19 traits::FileListResult,
20};
21
22fn md5_compute(input: &str) -> String {
24 use std::collections::hash_map::DefaultHasher;
25 use std::hash::{Hash, Hasher};
26
27 let mut hasher = DefaultHasher::new();
28 input.hash(&mut hasher);
29 format!("{:x}", hasher.finish())
30}
31
32pub struct LocalStorage {
34 base_dir: PathBuf,
35 config: LocalStorageConfig,
36}
37
38impl LocalStorage {
39 pub fn new(config: LocalStorageConfig) -> StorageResult<Self> {
41 let base_dir = PathBuf::from(&config.base_dir);
42
43 std::fs::create_dir_all(&base_dir).map_err(|e| {
45 StorageError::FilesystemError(std::io::Error::other(
46 format!("Failed to create base directory {}: {}", base_dir.display(), e)
47 ))
48 })?;
49
50 if !base_dir.exists() || !base_dir.is_dir() {
52 return Err(StorageError::InvalidConfiguration(
53 format!("Base directory is not a valid directory: {}", base_dir.display())
54 ));
55 }
56
57 info!("Local storage initialized with base directory: {}", base_dir.display());
58
59 Ok(Self { base_dir, config })
60 }
61
62 fn get_file_path(&self, key: &str) -> PathBuf {
64 let safe_key = sanitize_filename(key);
66 self.base_dir.join(safe_key)
67 }
68
69 fn get_file_metadata(&self, path: &Path, key: &str) -> StorageResult<StorageFile> {
71 let metadata = std::fs::metadata(path).map_err(|e| {
72 if e.kind() == std::io::ErrorKind::NotFound {
73 StorageError::FileNotFound(key.to_string())
74 } else {
75 StorageError::FilesystemError(e)
76 }
77 })?;
78
79 let modified = metadata.modified()
80 .map_err(StorageError::FilesystemError)?;
81 let created = metadata.created()
82 .unwrap_or(modified); let content_type = detect_mime_type(key);
85
86 Ok(StorageFile {
87 id: format!("local-{}", md5_compute(key)), name: key.to_string(),
89 size: metadata.len(),
90 content_type,
91 checksum: Some(format!("\"{}\"", metadata.len())), checksum_algorithm: Some("size".to_string()),
93 metadata: HashMap::new(),
94 created_at: DateTime::from(created),
95 updated_at: Some(DateTime::from(modified)),
96 version: None,
97 backend: crate::StorageBackend::Local,
98 bucket: "local".to_string(),
99 storage_path: key.to_string(),
100 encrypted: false,
101 compressed: false,
102 expires_at: None,
103 })
104 }
105
106 pub fn builder() -> LocalStorageBuilder {
108 LocalStorageBuilder::new()
109 }
110}
111
112#[async_trait]
113impl StorageService for LocalStorage {
114 async fn upload_bytes(
115 &self,
116 path: &str,
117 data: Bytes,
118 options: Option<UploadOptions>,
119 ) -> StorageResult<StorageFile> {
120 let file_path = self.get_file_path(path);
121
122 if let Some(parent) = file_path.parent() {
124 std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
125 }
126
127 tokio::fs::write(&file_path, data).await.map_err(StorageError::FilesystemError)?;
129
130 self.get_file_metadata(&file_path, path)
132 }
133
134 async fn upload_reader(
135 &self,
136 path: &str,
137 mut reader: Box<dyn tokio::io::AsyncRead + Send + Unpin>,
138 size: Option<u64>,
139 options: Option<UploadOptions>,
140 ) -> StorageResult<StorageFile> {
141 let file_path = self.get_file_path(path);
142
143 if let Some(parent) = file_path.parent() {
145 std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
146 }
147
148 let mut file = tokio::fs::File::create(&file_path).await.map_err(StorageError::FilesystemError)?;
150
151 tokio::io::copy(&mut reader, &mut file).await.map_err(StorageError::FilesystemError)?;
152
153 self.get_file_metadata(&file_path, path)
155 }
156
157 async fn upload_file(
158 &self,
159 path: &str,
160 local_path: &str,
161 options: Option<UploadOptions>,
162 ) -> StorageResult<StorageFile> {
163 let file_path = self.get_file_path(path);
164
165 if let Some(parent) = file_path.parent() {
167 std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
168 }
169
170 tokio::fs::copy(local_path, &file_path).await.map_err(StorageError::FilesystemError)?;
172
173 self.get_file_metadata(&file_path, path)
175 }
176
177 async fn download_bytes(
178 &self,
179 path: &str,
180 _options: Option<DownloadOptions>,
181 ) -> StorageResult<Bytes> {
182 let file_path = self.get_file_path(path);
183
184 if !file_path.exists() {
185 return Err(StorageError::FileNotFound(path.to_string()));
186 }
187
188 let data = tokio::fs::read(&file_path).await.map_err(StorageError::FilesystemError)?;
189 Ok(Bytes::from(data))
190 }
191
192 async fn download_writer(
193 &self,
194 path: &str,
195 mut writer: Box<dyn tokio::io::AsyncWrite + Send + Unpin>,
196 _options: Option<DownloadOptions>,
197 ) -> StorageResult<()> {
198 let file_path = self.get_file_path(path);
199
200 if !file_path.exists() {
201 return Err(StorageError::FileNotFound(path.to_string()));
202 }
203
204 let mut file = tokio::fs::File::open(&file_path).await.map_err(StorageError::FilesystemError)?;
205
206 tokio::io::copy(&mut file, &mut writer).await.map_err(StorageError::FilesystemError)?;
207 Ok(())
208 }
209
210 async fn download_file(
211 &self,
212 path: &str,
213 local_path: &str,
214 _options: Option<DownloadOptions>,
215 ) -> StorageResult<()> {
216 let file_path = self.get_file_path(path);
217
218 if !file_path.exists() {
219 return Err(StorageError::FileNotFound(path.to_string()));
220 }
221
222 if let Some(parent) = std::path::Path::new(local_path).parent() {
224 std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
225 }
226
227 tokio::fs::copy(&file_path, local_path).await.map_err(StorageError::FilesystemError)?;
228 Ok(())
229 }
230
231 async fn stream_file(
232 &self,
233 path: &str,
234 _range: Option<ByteRange>,
235 ) -> StorageResult<Box<dyn futures::stream::Stream<Item = StorageResult<Bytes>> + Send + Unpin>> {
236 let data = self.download_bytes(path, None).await?;
237 let stream = futures::stream::once(async { Ok(data) });
238 Ok(Box::new(Box::pin(stream)) as Box<dyn futures::stream::Stream<Item = StorageResult<Bytes>> + Send + Unpin>)
239 }
240
241 async fn get_file(&self, path: &str) -> StorageResult<StorageFile> {
242 let file_path = self.get_file_path(path);
243 self.get_file_metadata(&file_path, path)
244 }
245
246 async fn file_exists(&self, path: &str) -> StorageResult<bool> {
247 let file_path = self.get_file_path(path);
248 Ok(file_path.exists())
249 }
250
251 async fn delete_file(&self, path: &str) -> StorageResult<bool> {
252 let file_path = self.get_file_path(path);
253
254 if !file_path.exists() {
255 return Ok(false);
256 }
257
258 tokio::fs::remove_file(&file_path).await.map_err(StorageError::FilesystemError)?;
259 Ok(true)
260 }
261
262 async fn copy_file(&self, from_path: &str, to_path: &str) -> StorageResult<StorageFile> {
263 let source_path = self.get_file_path(from_path);
264 let dest_path = self.get_file_path(to_path);
265
266 if !source_path.exists() {
267 return Err(StorageError::FileNotFound(from_path.to_string()));
268 }
269
270 if let Some(parent) = dest_path.parent() {
272 std::fs::create_dir_all(parent).map_err(StorageError::FilesystemError)?;
273 }
274
275 tokio::fs::copy(&source_path, &dest_path).await.map_err(StorageError::FilesystemError)?;
276 self.get_file_metadata(&dest_path, to_path)
277 }
278
279 async fn move_file(&self, from_path: &str, to_path: &str) -> StorageResult<StorageFile> {
280 let result = self.copy_file(from_path, to_path).await?;
281 let _ = self.delete_file(from_path).await?;
282 Ok(result)
283 }
284
285 async fn list_files(
286 &self,
287 prefix: &str,
288 limit: Option<u32>,
289 continuation_token: Option<String>,
290 ) -> StorageResult<FileListResult> {
291 let mut files = Vec::new();
292 let start_after = continuation_token.unwrap_or_default();
293
294 let mut entries = tokio::fs::read_dir(&self.base_dir).await.map_err(StorageError::FilesystemError)?;
295
296 while let Ok(Some(entry)) = entries.next_entry().await {
297 let path = entry.path();
298
299 if path.is_dir() {
300 continue;
301 }
302
303 let key = path.strip_prefix(&self.base_dir)
304 .unwrap_or(&path)
305 .to_string_lossy()
306 .to_string();
307
308 if !key.starts_with(prefix) || key <= start_after {
309 continue;
310 }
311
312 if let Ok(file) = self.get_file_metadata(&path, &key) {
313 files.push(file);
314 }
315
316 if let Some(limit) = limit {
317 if files.len() >= limit as usize {
318 break;
319 }
320 }
321 }
322
323 let files_count = files.len() as u64;
324 let next_continuation_token = if let Some(limit) = limit {
325 if files.len() >= limit as usize {
326 files.last().map(|f| f.storage_path.clone())
327 } else {
328 None
329 }
330 } else {
331 None
332 };
333
334 Ok(FileListResult {
335 files,
336 prefixes: Vec::new(),
337 truncated: next_continuation_token.is_some(),
338 next_continuation_token,
339 total_count: Some(files_count),
340 })
341 }
342
343 async fn generate_presigned_url(
344 &self,
345 path: &str,
346 options: PresignedUrlOptions,
347 ) -> StorageResult<String> {
348 let file_path = self.get_file_path(path);
349
350 if !file_path.exists() {
351 return Err(StorageError::FileNotFound(path.to_string()));
352 }
353
354 Ok(format!("file://{}", file_path.display()))
356 }
357
358 async fn get_file_url(
359 &self,
360 path: &str,
361 _expiry_seconds: Option<u64>,
362 ) -> StorageResult<String> {
363 self.generate_presigned_url(path, PresignedUrlOptions::default()).await
364 }
365
366 async fn update_metadata(
367 &self,
368 path: &str,
369 _metadata: HashMap<String, String>,
370 ) -> StorageResult<StorageFile> {
371 self.get_file(path).await
374 }
375
376 async fn get_stats(&self) -> StorageResult<StorageStats> {
377 let mut total_files = 0u64;
378 let mut total_size = 0u64;
379 let mut backend_usage = HashMap::new();
380 let mut content_type_counts = HashMap::new();
381
382 let mut entries = tokio::fs::read_dir(&self.base_dir).await.map_err(StorageError::FilesystemError)?;
383
384 while let Ok(Some(entry)) = entries.next_entry().await {
385 let path = entry.path();
386
387 if path.is_file() {
388 total_files += 1;
389 if let Ok(metadata) = entry.metadata().await {
390 total_size += metadata.len();
391 }
392
393 if let Some(ext) = path.extension() {
395 let content_type = detect_mime_type(&path.to_string_lossy());
396 *content_type_counts.entry(content_type).or_insert(0) += 1;
397 }
398 }
399 }
400
401 backend_usage.insert("local".to_string(), total_size);
402
403 Ok(StorageStats {
404 file_count: total_files,
405 total_bytes: total_size,
406 backend_usage,
407 content_type_counts,
408 average_file_size: if total_files > 0 { total_size as f64 / total_files as f64 } else { 0.0 },
409 largest_file_size: 0, last_updated: chrono::Utc::now(),
411 })
412 }
413
414 async fn test_connection(&self) -> StorageResult<bool> {
415 Ok(self.base_dir.exists() && self.base_dir.is_dir())
417 }
418
419 fn backend_type(&self) -> crate::StorageBackend {
420 crate::StorageBackend::Local
421 }
422
423 fn bucket(&self) -> &str {
424 "local"
425 }
426
427 fn config(&self) -> &StorageConfig {
428 static DEFAULT_CONFIG: std::sync::OnceLock<StorageConfig> = std::sync::OnceLock::new();
429 DEFAULT_CONFIG.get_or_init(|| StorageConfig {
430 backend: crate::StorageBackend::Local,
431 bucket: "local".to_string(),
432 base_path: Some(self.config.base_dir.clone()),
433 multipart_threshold: 100 * 1024 * 1024, chunk_size: 8 * 1024 * 1024, versioning: false,
436 compression: false,
437 encryption: None,
438 retention: None,
439 access_control: None,
440 backend_config: HashMap::new(),
441 #[cfg(feature = "compression")]
442 compression_config: None,
443 })
444 }
445}
446
447pub struct LocalStorageBuilder {
449 base_dir: Option<String>,
450 compression: bool,
451}
452
453impl LocalStorageBuilder {
454 pub fn new() -> Self {
456 Self {
457 base_dir: None,
458 compression: false,
459 }
460 }
461
462 pub fn base_dir(mut self, dir: impl Into<String>) -> Self {
464 self.base_dir = Some(dir.into());
465 self
466 }
467
468 pub fn compression(mut self, enabled: bool) -> Self {
470 self.compression = enabled;
471 self
472 }
473
474 pub fn build(self) -> StorageResult<LocalStorage> {
476 let base_dir = self.base_dir
477 .ok_or_else(|| StorageError::InvalidConfiguration("Base directory is required".to_string()))?;
478
479 let config = LocalStorageConfig {
480 base_dir,
481 base_path: None,
482 compression: self.compression,
483 encryption: None,
484 };
485
486 LocalStorage::new(config)
487 }
488}
489
490impl Default for LocalStorageBuilder {
491 fn default() -> Self {
492 Self::new()
493 }
494}