1use crate::{StorageBackend, StorageResult, StorageError, FileMetadata, UploadOptions, StorageStats};
4use crate::config::LocalStorageConfig;
5use async_trait::async_trait;
6use bytes::Bytes;
7use futures::{Stream, StreamExt};
8use std::path::{Path, PathBuf};
9use std::time::Duration;
10use tokio::fs;
11use tokio::io::{AsyncReadExt, AsyncWriteExt};
12use chrono::Utc;
13use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
17pub struct LocalBackend {
18 config: LocalStorageConfig,
19}
20
21impl LocalBackend {
22 pub fn new(config: LocalStorageConfig) -> Self {
24 Self { config }
25 }
26
27 fn full_path(&self, path: &str) -> PathBuf {
29 let sanitized = self.sanitize_path(path);
31 self.config.root_path.join(sanitized)
32 }
33
34 fn sanitize_path(&self, path: &str) -> PathBuf {
36 let path = path.trim_start_matches('/');
37 let components: Vec<&str> = path
38 .split('/')
39 .filter(|component| !component.is_empty() && *component != "." && *component != "..")
40 .collect();
41
42 components.iter().collect()
43 }
44
45 async fn ensure_parent_dir(&self, file_path: &Path) -> StorageResult<()> {
47 if !self.config.create_directories {
48 return Ok(());
49 }
50
51 if let Some(parent) = file_path.parent() {
52 if !parent.exists() {
53 fs::create_dir_all(parent).await.map_err(|e| {
54 StorageError::Io(std::io::Error::new(
55 std::io::ErrorKind::PermissionDenied,
56 format!("Failed to create directory {}: {}", parent.display(), e)
57 ))
58 })?;
59
60 #[cfg(unix)]
61 if let Some(permissions) = self.config.directory_permissions {
62 use std::os::unix::fs::PermissionsExt;
63 let perms = std::fs::Permissions::from_mode(permissions);
64 std::fs::set_permissions(parent, perms).map_err(|e| {
65 StorageError::Io(std::io::Error::new(
66 std::io::ErrorKind::PermissionDenied,
67 format!("Failed to set directory permissions: {}", e)
68 ))
69 })?;
70 }
71 }
72 }
73 Ok(())
74 }
75
76 #[cfg(unix)]
78 async fn set_file_permissions(&self, file_path: &Path) -> StorageResult<()> {
79 if let Some(permissions) = self.config.file_permissions {
80 use std::os::unix::fs::PermissionsExt;
81 let perms = std::fs::Permissions::from_mode(permissions);
82 fs::set_permissions(file_path, perms).await.map_err(|e| {
83 StorageError::Io(std::io::Error::new(
84 std::io::ErrorKind::PermissionDenied,
85 format!("Failed to set file permissions: {}", e)
86 ))
87 })?;
88 }
89 Ok(())
90 }
91
92 #[cfg(not(unix))]
93 async fn set_file_permissions(&self, _file_path: &Path) -> StorageResult<()> {
94 Ok(())
95 }
96
97 async fn generate_etag(&self, file_path: &Path) -> StorageResult<String> {
99 let metadata = fs::metadata(file_path).await?;
100 let size = metadata.len();
101 let modified = metadata.modified()
102 .map_err(|e| StorageError::Io(e))?
103 .duration_since(std::time::UNIX_EPOCH)
104 .map_err(|e| StorageError::Backend(format!("Time error: {}", e)))?
105 .as_secs();
106
107 Ok(format!("{}-{}", size, modified))
108 }
109}
110
111#[async_trait]
112impl StorageBackend for LocalBackend {
113 async fn put(&self, path: &str, data: &[u8], options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
114 let file_path = self.full_path(path);
115
116 if let Some(opts) = &options {
118 if !opts.overwrite && file_path.exists() {
119 return Err(StorageError::Backend(format!("File already exists: {}", path)));
120 }
121 }
122
123 self.ensure_parent_dir(&file_path).await?;
125
126 fs::write(&file_path, data).await?;
128
129 self.set_file_permissions(&file_path).await?;
131
132 let content_type = options
134 .as_ref()
135 .and_then(|o| o.content_type.clone())
136 .unwrap_or_else(|| crate::detect_content_type(path, data));
137
138 let etag = self.generate_etag(&file_path).await?;
139 let now = Utc::now();
140
141 let metadata = FileMetadata {
142 path: path.to_string(),
143 size: data.len() as u64,
144 content_type,
145 created_at: now,
146 modified_at: now,
147 etag: Some(etag),
148 metadata: options
149 .as_ref()
150 .map(|o| o.metadata.clone())
151 .unwrap_or_default(),
152 #[cfg(feature = "access-control")]
153 permissions: options.as_ref().and_then(|o| o.permissions.clone()),
154 };
155
156 if !metadata.metadata.is_empty() {
158 let metadata_path = format!("{}.metadata", file_path.display());
159 let metadata_json = serde_json::to_string(&metadata.metadata)
160 .map_err(|e| StorageError::Backend(format!("Failed to serialize metadata: {}", e)))?;
161 fs::write(&metadata_path, metadata_json).await?;
162 }
163
164 Ok(metadata)
165 }
166
167 async fn put_stream<S>(&self, path: &str, mut stream: S, options: Option<UploadOptions>) -> StorageResult<FileMetadata>
168 where
169 S: Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin,
170 {
171 let file_path = self.full_path(path);
172
173 if let Some(opts) = &options {
175 if !opts.overwrite && file_path.exists() {
176 return Err(StorageError::Backend(format!("File already exists: {}", path)));
177 }
178 }
179
180 self.ensure_parent_dir(&file_path).await?;
182
183 let mut file = fs::File::create(&file_path).await?;
185 let mut total_size = 0u64;
186
187 while let Some(chunk) = stream.next().await {
188 let chunk = chunk?;
189 file.write_all(&chunk).await?;
190 total_size += chunk.len() as u64;
191 }
192
193 file.flush().await?;
194 drop(file);
195
196 self.set_file_permissions(&file_path).await?;
198
199 let content_type = options
201 .as_ref()
202 .and_then(|o| o.content_type.clone())
203 .unwrap_or_else(|| {
204 crate::detect_content_type(path, &[])
206 });
207
208 let etag = self.generate_etag(&file_path).await?;
209 let now = Utc::now();
210
211 let metadata = FileMetadata {
212 path: path.to_string(),
213 size: total_size,
214 content_type,
215 created_at: now,
216 modified_at: now,
217 etag: Some(etag),
218 metadata: options
219 .as_ref()
220 .map(|o| o.metadata.clone())
221 .unwrap_or_default(),
222 #[cfg(feature = "access-control")]
223 permissions: options.as_ref().and_then(|o| o.permissions.clone()),
224 };
225
226 if !metadata.metadata.is_empty() {
228 let metadata_path = format!("{}.metadata", file_path.display());
229 let metadata_json = serde_json::to_string(&metadata.metadata)
230 .map_err(|e| StorageError::Backend(format!("Failed to serialize metadata: {}", e)))?;
231 fs::write(&metadata_path, metadata_json).await?;
232 }
233
234 Ok(metadata)
235 }
236
237 async fn get(&self, path: &str) -> StorageResult<Option<Bytes>> {
238 let file_path = self.full_path(path);
239
240 if !file_path.exists() {
241 return Ok(None);
242 }
243
244 let data = fs::read(&file_path).await?;
245 Ok(Some(Bytes::from(data)))
246 }
247
248 async fn get_stream(&self, path: &str) -> StorageResult<Option<Box<dyn Stream<Item = Result<Bytes, std::io::Error>> + Send + Unpin>>> {
249 let file_path = self.full_path(path);
250
251 if !file_path.exists() {
252 return Ok(None);
253 }
254
255 let file = fs::File::open(&file_path).await?;
256 let stream = tokio_util::io::ReaderStream::new(file);
257 let byte_stream = stream.map(|result| result.map(Bytes::from));
258
259 Ok(Some(Box::new(byte_stream)))
260 }
261
262 async fn exists(&self, path: &str) -> StorageResult<bool> {
263 let file_path = self.full_path(path);
264 Ok(file_path.exists())
265 }
266
267 async fn metadata(&self, path: &str) -> StorageResult<Option<FileMetadata>> {
268 let file_path = self.full_path(path);
269
270 if !file_path.exists() {
271 return Ok(None);
272 }
273
274 let fs_metadata = fs::metadata(&file_path).await?;
275 let size = fs_metadata.len();
276
277 let created_at = fs_metadata.created()
278 .map_err(|e| StorageError::Io(e))?
279 .into();
280
281 let modified_at = fs_metadata.modified()
282 .map_err(|e| StorageError::Io(e))?
283 .into();
284
285 let etag = self.generate_etag(&file_path).await?;
286
287 let metadata_path = format!("{}.metadata", file_path.display());
289 let custom_metadata = if Path::new(&metadata_path).exists() {
290 let metadata_json = fs::read_to_string(&metadata_path).await?;
291 serde_json::from_str::<HashMap<String, String>>(&metadata_json)
292 .unwrap_or_default()
293 } else {
294 HashMap::new()
295 };
296
297 let content_type = if size <= 1024 * 1024 {
299 let sample = fs::read(&file_path).await.unwrap_or_default();
301 crate::detect_content_type(path, &sample)
302 } else {
303 crate::detect_content_type(path, &[])
305 };
306
307 let file_metadata = FileMetadata {
308 path: path.to_string(),
309 size,
310 content_type,
311 created_at,
312 modified_at,
313 etag: Some(etag),
314 metadata: custom_metadata,
315 #[cfg(feature = "access-control")]
316 permissions: None, };
318
319 Ok(Some(file_metadata))
320 }
321
322 async fn delete(&self, path: &str) -> StorageResult<bool> {
323 let file_path = self.full_path(path);
324
325 if !file_path.exists() {
326 return Ok(false);
327 }
328
329 fs::remove_file(&file_path).await?;
330
331 let metadata_path = format!("{}.metadata", file_path.display());
333 if Path::new(&metadata_path).exists() {
334 let _ = fs::remove_file(&metadata_path).await;
335 }
336
337 Ok(true)
338 }
339
340 async fn list(&self, prefix: Option<&str>, limit: Option<u32>) -> StorageResult<Vec<FileMetadata>> {
341 let search_path = if let Some(prefix) = prefix {
342 self.full_path(prefix)
343 } else {
344 self.config.root_path.clone()
345 };
346
347 let mut files = Vec::new();
348 let limit = limit.unwrap_or(1000) as usize;
349
350 fn collect_files<'a>(
351 dir: &'a Path,
352 root: &'a Path,
353 files: &'a mut Vec<FileMetadata>,
354 limit: usize,
355 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = StorageResult<()>> + Send + 'a>> {
356 Box::pin(async move {
357 if files.len() >= limit {
358 return Ok(());
359 }
360
361 let mut entries = fs::read_dir(dir).await?;
362 while let Some(entry) = entries.next_entry().await? {
363 if files.len() >= limit {
364 break;
365 }
366
367 let path = entry.path();
368
369 if path.is_file() {
370 if path.extension().and_then(|e| e.to_str()) == Some("metadata") {
372 continue;
373 }
374
375 let relative_path = path.strip_prefix(root)
376 .map_err(|e| StorageError::Backend(format!("Path error: {}", e)))?;
377
378 let relative_str = relative_path.to_string_lossy().replace('\\', "/");
379
380 if let Ok(Some(metadata)) = LocalBackend::metadata_for_path(&path, &relative_str).await {
382 files.push(metadata);
383 }
384 } else if path.is_dir() {
385 collect_files(&path, root, files, limit).await?;
386 }
387 }
388
389 Ok(())
390 })
391 }
392
393 collect_files(&search_path, &self.config.root_path, &mut files, limit).await?;
394
395 Ok(files)
396 }
397
398 async fn copy(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
399 let from_path = self.full_path(from);
400 let to_path = self.full_path(to);
401
402 if !from_path.exists() {
403 return Err(StorageError::FileNotFound(from.to_string()));
404 }
405
406 if let Some(opts) = &options {
408 if !opts.overwrite && to_path.exists() {
409 return Err(StorageError::Backend(format!("File already exists: {}", to)));
410 }
411 }
412
413 self.ensure_parent_dir(&to_path).await?;
415
416 fs::copy(&from_path, &to_path).await?;
418
419 self.set_file_permissions(&to_path).await?;
421
422 let from_metadata_path = format!("{}.metadata", from_path.display());
424 let to_metadata_path = format!("{}.metadata", to_path.display());
425
426 if Path::new(&from_metadata_path).exists() {
427 let _ = fs::copy(&from_metadata_path, &to_metadata_path).await;
428 }
429
430 self.metadata(to).await?
432 .ok_or_else(|| StorageError::Backend("Failed to get metadata for copied file".to_string()))
433 }
434
435 async fn move_file(&self, from: &str, to: &str, options: Option<UploadOptions>) -> StorageResult<FileMetadata> {
436 let from_path = self.full_path(from);
437 let to_path = self.full_path(to);
438
439 if !from_path.exists() {
440 return Err(StorageError::FileNotFound(from.to_string()));
441 }
442
443 if let Some(opts) = &options {
445 if !opts.overwrite && to_path.exists() {
446 return Err(StorageError::Backend(format!("File already exists: {}", to)));
447 }
448 }
449
450 self.ensure_parent_dir(&to_path).await?;
452
453 fs::rename(&from_path, &to_path).await?;
455
456 let from_metadata_path = format!("{}.metadata", from_path.display());
458 let to_metadata_path = format!("{}.metadata", to_path.display());
459
460 if Path::new(&from_metadata_path).exists() {
461 let _ = fs::rename(&from_metadata_path, &to_metadata_path).await;
462 }
463
464 self.metadata(to).await?
466 .ok_or_else(|| StorageError::Backend("Failed to get metadata for moved file".to_string()))
467 }
468
469 async fn signed_url(&self, path: &str, _expires_in: Duration) -> StorageResult<String> {
470 let file_path = self.full_path(path);
472 if !file_path.exists() {
473 return Err(StorageError::FileNotFound(path.to_string()));
474 }
475
476 Ok(format!("file://{}", file_path.display()))
477 }
478
479 async fn public_url(&self, path: &str) -> StorageResult<String> {
480 let file_path = self.full_path(path);
482 if !file_path.exists() {
483 return Err(StorageError::FileNotFound(path.to_string()));
484 }
485
486 Ok(format!("file://{}", file_path.display()))
487 }
488
489 async fn stats(&self) -> StorageResult<StorageStats> {
490 let mut total_files = 0u64;
491 let mut total_size = 0u64;
492
493 fn collect_stats<'a>(dir: &'a Path, stats: &'a mut (u64, u64)) -> std::pin::Pin<Box<dyn std::future::Future<Output = StorageResult<()>> + Send + 'a>> {
494 Box::pin(async move {
495 let mut entries = fs::read_dir(dir).await?;
496 while let Some(entry) = entries.next_entry().await? {
497 let path = entry.path();
498
499 if path.is_file() {
500 if path.extension().and_then(|e| e.to_str()) == Some("metadata") {
502 continue;
503 }
504
505 let metadata = fs::metadata(&path).await?;
506 stats.0 += 1;
507 stats.1 += metadata.len();
508 } else if path.is_dir() {
509 collect_stats(&path, stats).await?;
510 }
511 }
512
513 Ok(())
514 })
515 }
516
517 let mut stats = (0u64, 0u64);
518 collect_stats(&self.config.root_path, &mut stats).await?;
519
520 let (available_space, used_space) = if let Ok(metadata) = fs::metadata(&self.config.root_path).await {
522 (None, None)
524 } else {
525 (None, None)
526 };
527
528 Ok(StorageStats {
529 total_files: stats.0,
530 total_size: stats.1,
531 available_space,
532 used_space,
533 })
534 }
535}
536
537impl LocalBackend {
538 async fn metadata_for_path(file_path: &Path, relative_path: &str) -> StorageResult<Option<FileMetadata>> {
540 if !file_path.exists() {
541 return Ok(None);
542 }
543
544 let fs_metadata = fs::metadata(file_path).await?;
545 let size = fs_metadata.len();
546
547 let created_at = fs_metadata.created()
548 .map_err(|e| StorageError::Io(e))?
549 .into();
550
551 let modified_at = fs_metadata.modified()
552 .map_err(|e| StorageError::Io(e))?
553 .into();
554
555 let modified_timestamp = fs_metadata.modified()
557 .map_err(|e| StorageError::Io(e))?
558 .duration_since(std::time::UNIX_EPOCH)
559 .map_err(|e| StorageError::Backend(format!("Time error: {}", e)))?
560 .as_secs();
561 let etag = format!("{}-{}", size, modified_timestamp);
562
563 let metadata_path = format!("{}.metadata", file_path.display());
565 let custom_metadata = if Path::new(&metadata_path).exists() {
566 let metadata_json = fs::read_to_string(&metadata_path).await?;
567 serde_json::from_str::<HashMap<String, String>>(&metadata_json)
568 .unwrap_or_default()
569 } else {
570 HashMap::new()
571 };
572
573 let content_type = crate::detect_content_type(relative_path, &[]);
575
576 let file_metadata = FileMetadata {
577 path: relative_path.to_string(),
578 size,
579 content_type,
580 created_at,
581 modified_at,
582 etag: Some(etag),
583 metadata: custom_metadata,
584 #[cfg(feature = "access-control")]
585 permissions: None,
586 };
587
588 Ok(Some(file_metadata))
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use tempfile::tempdir;
596
597 async fn create_test_backend() -> (LocalBackend, tempfile::TempDir) {
598 let temp_dir = tempdir().unwrap();
599 let config = LocalStorageConfig::new()
600 .with_root_path(temp_dir.path().to_path_buf());
601 let backend = LocalBackend::new(config);
602 (backend, temp_dir)
603 }
604
605 #[tokio::test]
606 async fn test_put_and_get() {
607 let (backend, _temp_dir) = create_test_backend().await;
608
609 let data = b"Hello, World!";
610 let metadata = backend.put("test.txt", data, None).await.unwrap();
611
612 assert_eq!(metadata.path, "test.txt");
613 assert_eq!(metadata.size, data.len() as u64);
614 assert_eq!(metadata.content_type, "text/plain");
615
616 let retrieved = backend.get("test.txt").await.unwrap().unwrap();
617 assert_eq!(retrieved.as_ref(), data);
618 }
619
620 #[tokio::test]
621 async fn test_exists_and_delete() {
622 let (backend, _temp_dir) = create_test_backend().await;
623
624 let data = b"Test data";
625 backend.put("test.txt", data, None).await.unwrap();
626
627 assert!(backend.exists("test.txt").await.unwrap());
628 assert!(!backend.exists("nonexistent.txt").await.unwrap());
629
630 assert!(backend.delete("test.txt").await.unwrap());
631 assert!(!backend.exists("test.txt").await.unwrap());
632 assert!(!backend.delete("nonexistent.txt").await.unwrap());
633 }
634
635 #[tokio::test]
636 async fn test_metadata() {
637 let (backend, _temp_dir) = create_test_backend().await;
638
639 let data = b"Test metadata";
640 let options = UploadOptions::new()
641 .content_type("text/custom".to_string())
642 .metadata("key1".to_string(), "value1".to_string())
643 .metadata("key2".to_string(), "value2".to_string());
644
645 backend.put("test.txt", data, Some(options)).await.unwrap();
646
647 let metadata = backend.metadata("test.txt").await.unwrap().unwrap();
648 assert_eq!(metadata.path, "test.txt");
649 assert_eq!(metadata.size, data.len() as u64);
650 assert!(metadata.etag.is_some());
651 assert_eq!(metadata.metadata.get("key1"), Some(&"value1".to_string()));
652 assert_eq!(metadata.metadata.get("key2"), Some(&"value2".to_string()));
653 }
654
655 #[tokio::test]
656 async fn test_copy_and_move() {
657 let (backend, _temp_dir) = create_test_backend().await;
658
659 let data = b"Test copy/move";
660 backend.put("original.txt", data, None).await.unwrap();
661
662 backend.copy("original.txt", "copy.txt", None).await.unwrap();
664 assert!(backend.exists("original.txt").await.unwrap());
665 assert!(backend.exists("copy.txt").await.unwrap());
666
667 let copied_data = backend.get("copy.txt").await.unwrap().unwrap();
668 assert_eq!(copied_data.as_ref(), data);
669
670 backend.move_file("original.txt", "moved.txt", None).await.unwrap();
672 assert!(!backend.exists("original.txt").await.unwrap());
673 assert!(backend.exists("moved.txt").await.unwrap());
674
675 let moved_data = backend.get("moved.txt").await.unwrap().unwrap();
676 assert_eq!(moved_data.as_ref(), data);
677 }
678
679 #[tokio::test]
680 async fn test_list() {
681 let (backend, _temp_dir) = create_test_backend().await;
682
683 backend.put("dir1/file1.txt", b"content1", None).await.unwrap();
685 backend.put("dir1/file2.txt", b"content2", None).await.unwrap();
686 backend.put("dir2/file3.txt", b"content3", None).await.unwrap();
687
688 let files = backend.list(None, None).await.unwrap();
690 assert_eq!(files.len(), 3);
691
692 let paths: Vec<String> = files.iter().map(|f| f.path.clone()).collect();
694 assert!(paths.contains(&"dir1/file1.txt".to_string()));
695 assert!(paths.contains(&"dir1/file2.txt".to_string()));
696 assert!(paths.contains(&"dir2/file3.txt".to_string()));
697 }
698
699 #[tokio::test]
700 async fn test_sanitize_path() {
701 let (backend, _temp_dir) = create_test_backend().await;
702
703 assert_eq!(backend.sanitize_path("../../../etc/passwd"), PathBuf::from("etc/passwd"));
705 assert_eq!(backend.sanitize_path("./test/../file.txt"), PathBuf::from("test/file.txt"));
706 assert_eq!(backend.sanitize_path("normal/path/file.txt"), PathBuf::from("normal/path/file.txt"));
707 assert_eq!(backend.sanitize_path("/absolute/path"), PathBuf::from("absolute/path"));
708 }
709}