1use super::backend::Storage;
4use super::errors::{StorageError, StorageResult};
5use super::file::{FileMetadata, StoredFile};
6use async_trait::async_trait;
7use sha2::{Digest, Sha256};
8use std::path::{Path, PathBuf};
9use tokio::fs;
10
11pub struct LocalStorage {
13 base_path: PathBuf,
14 base_url: String,
15}
16
17impl LocalStorage {
18 pub fn new(base_path: impl Into<PathBuf>, base_url: impl Into<String>) -> Self {
29 Self {
30 base_path: base_path.into(),
31 base_url: base_url.into(),
32 }
33 }
34 pub async fn ensure_base_dir(&self) -> StorageResult<()> {
52 fs::create_dir_all(&self.base_path).await?;
53 Ok(())
54 }
55
56 fn full_path(&self, path: &str) -> PathBuf {
57 self.base_path.join(path)
58 }
59
60 fn validate_path(path: &str) -> StorageResult<()> {
62 if path.starts_with('/') || path.starts_with('\\') {
64 return Err(StorageError::InvalidPath(format!(
65 "Detected path traversal attempt in '{}'",
66 path
67 )));
68 }
69
70 let path_obj = Path::new(path);
72 for component in path_obj.components() {
73 if component == std::path::Component::ParentDir {
74 return Err(StorageError::InvalidPath(format!(
75 "Detected path traversal attempt in '{}'",
76 path
77 )));
78 }
79 }
80
81 if path == "." || path == ".." || path.is_empty() {
83 return Err(StorageError::InvalidPath(format!(
84 "Could not derive file name from '{}'",
85 path
86 )));
87 }
88
89 Ok(())
90 }
91
92 fn compute_checksum(content: &[u8]) -> String {
93 let mut hasher = Sha256::new();
94 hasher.update(content);
95 hex::encode(hasher.finalize())
96 }
97}
98
99#[async_trait]
100impl Storage for LocalStorage {
101 async fn save(&self, path: &str, content: &[u8]) -> StorageResult<FileMetadata> {
102 Self::validate_path(path)?;
104
105 let full_path = self.full_path(path);
106
107 if let Some(parent) = full_path.parent() {
109 fs::create_dir_all(parent).await?;
110 }
111
112 fs::write(&full_path, content).await?;
114
115 let file_meta = fs::metadata(&full_path).await?;
117 let size = file_meta.len();
118 let checksum = Self::compute_checksum(content);
119
120 Ok(FileMetadata::new(path.to_string(), size).with_checksum(checksum))
121 }
122
123 async fn read(&self, path: &str) -> StorageResult<StoredFile> {
124 Self::validate_path(path)?;
126
127 let full_path = self.full_path(path);
128
129 if !full_path.exists() {
130 return Err(StorageError::NotFound(path.to_string()));
131 }
132
133 let content = fs::read(&full_path).await?;
134 let file_meta = fs::metadata(&full_path).await?;
135 let size = file_meta.len();
136
137 let metadata = FileMetadata::new(path.to_string(), size);
138 Ok(StoredFile::new(metadata, content))
139 }
140
141 async fn delete(&self, path: &str) -> StorageResult<()> {
142 Self::validate_path(path)?;
144
145 let full_path = self.full_path(path);
146
147 if !full_path.exists() {
148 return Err(StorageError::NotFound(path.to_string()));
149 }
150
151 fs::remove_file(&full_path).await?;
152 Ok(())
153 }
154
155 async fn exists(&self, path: &str) -> StorageResult<bool> {
156 Self::validate_path(path)?;
158
159 let full_path = self.full_path(path);
160 Ok(full_path.exists())
161 }
162
163 async fn metadata(&self, path: &str) -> StorageResult<FileMetadata> {
164 Self::validate_path(path)?;
166
167 let full_path = self.full_path(path);
168
169 if !full_path.exists() {
170 return Err(StorageError::NotFound(path.to_string()));
171 }
172
173 let file_meta = fs::metadata(&full_path).await?;
174 let size = file_meta.len();
175
176 Ok(FileMetadata::new(path.to_string(), size))
177 }
178
179 async fn list(&self, path: &str) -> StorageResult<Vec<FileMetadata>> {
180 if !path.is_empty() {
183 Self::validate_path(path)?;
184 }
185
186 let full_path = self.full_path(path);
187 let mut entries = fs::read_dir(&full_path).await?;
188 let mut results = Vec::new();
189
190 while let Some(entry) = entries.next_entry().await? {
191 let metadata = entry.metadata().await?;
192 if metadata.is_file() {
193 let file_name = entry.file_name().to_string_lossy().to_string();
194 let relative_path = Path::new(path).join(&file_name);
195 results.push(FileMetadata::new(
196 relative_path.to_string_lossy().to_string(),
197 metadata.len(),
198 ));
199 }
200 }
201
202 Ok(results)
203 }
204
205 fn url(&self, path: &str) -> String {
206 format!(
207 "{}/{}",
208 self.base_url.trim_end_matches('/'),
209 path.trim_start_matches('/')
210 )
211 }
212
213 fn path(&self, name: &str) -> String {
214 name.to_string()
215 }
216
217 async fn get_accessed_time(&self, path: &str) -> StorageResult<chrono::DateTime<chrono::Utc>> {
218 Self::validate_path(path)?;
220
221 let full_path = self.full_path(path);
222
223 if !full_path.exists() {
224 return Err(StorageError::NotFound(path.to_string()));
225 }
226
227 let file_meta = fs::metadata(&full_path).await?;
228 let accessed = file_meta.accessed()?;
229 let datetime: chrono::DateTime<chrono::Utc> = accessed.into();
230 Ok(datetime)
231 }
232
233 async fn get_created_time(&self, path: &str) -> StorageResult<chrono::DateTime<chrono::Utc>> {
234 Self::validate_path(path)?;
236
237 let full_path = self.full_path(path);
238
239 if !full_path.exists() {
240 return Err(StorageError::NotFound(path.to_string()));
241 }
242
243 let file_meta = fs::metadata(&full_path).await?;
244 let created = file_meta.created()?;
245 let datetime: chrono::DateTime<chrono::Utc> = created.into();
246 Ok(datetime)
247 }
248
249 async fn get_modified_time(&self, path: &str) -> StorageResult<chrono::DateTime<chrono::Utc>> {
250 Self::validate_path(path)?;
252
253 let full_path = self.full_path(path);
254
255 if !full_path.exists() {
256 return Err(StorageError::NotFound(path.to_string()));
257 }
258
259 let file_meta = fs::metadata(&full_path).await?;
260 let modified = file_meta.modified()?;
261 let datetime: chrono::DateTime<chrono::Utc> = modified.into();
262 Ok(datetime)
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use tempfile::TempDir;
270
271 async fn create_test_storage() -> (LocalStorage, TempDir) {
272 let temp_dir = TempDir::new().unwrap();
273 let storage = LocalStorage::new(temp_dir.path(), "http://localhost/media");
274 storage.ensure_base_dir().await.unwrap();
275 (storage, temp_dir)
276 }
277
278 #[tokio::test]
279 async fn test_local_storage_path() {
280 let storage = LocalStorage::new("/tmp/storage", "http://localhost/media");
281 assert_eq!(storage.url("test.txt"), "http://localhost/media/test.txt");
282 }
283
284 #[tokio::test]
285 async fn test_file_access_options() {
286 let (storage, _temp_dir) = create_test_storage().await;
287
288 assert!(!storage.exists("storage_test").await.unwrap());
290
291 let content = b"storage contents";
293 storage.save("storage_test", content).await.unwrap();
294
295 assert!(storage.exists("storage_test").await.unwrap());
297
298 let file = storage.read("storage_test").await.unwrap();
300 assert_eq!(file.content, content);
301
302 storage.delete("storage_test").await.unwrap();
304 assert!(!storage.exists("storage_test").await.unwrap());
305 }
306
307 #[tokio::test]
308 async fn test_file_save_with_path() {
309 let (storage, _temp_dir) = create_test_storage().await;
310
311 assert!(!storage.exists("path/to").await.unwrap());
313
314 storage
315 .save("path/to/test.file", b"file saved with path")
316 .await
317 .unwrap();
318
319 assert!(storage.exists("path/to/test.file").await.unwrap());
320
321 let file = storage.read("path/to/test.file").await.unwrap();
322 assert_eq!(file.content, b"file saved with path");
323
324 storage.delete("path/to/test.file").await.unwrap();
325 }
326
327 #[tokio::test]
328 async fn test_file_size() {
329 let (storage, _temp_dir) = create_test_storage().await;
330
331 storage.save("file.txt", b"test").await.unwrap();
332 let metadata = storage.metadata("file.txt").await.unwrap();
333 assert_eq!(metadata.size, 4);
334
335 storage.delete("file.txt").await.unwrap();
336 }
337
338 #[tokio::test]
339 async fn test_exists() {
340 let (storage, _temp_dir) = create_test_storage().await;
341
342 storage.save("dir/subdir/file.txt", b"test").await.unwrap();
343 assert!(storage.exists("dir/subdir/file.txt").await.unwrap());
344
345 storage.delete("dir/subdir/file.txt").await.unwrap();
346 }
347
348 #[tokio::test]
349 async fn test_delete() {
350 let (storage, _temp_dir) = create_test_storage().await;
351
352 storage.save("dir/subdir/file.txt", b"test").await.unwrap();
353 storage
354 .save("dir/subdir/other_file.txt", b"test")
355 .await
356 .unwrap();
357
358 assert!(storage.exists("dir/subdir/file.txt").await.unwrap());
359 assert!(storage.exists("dir/subdir/other_file.txt").await.unwrap());
360
361 storage.delete("dir/subdir/other_file.txt").await.unwrap();
362 assert!(!storage.exists("dir/subdir/other_file.txt").await.unwrap());
363
364 storage.delete("dir/subdir/file.txt").await.unwrap();
365 assert!(!storage.exists("dir/subdir/file.txt").await.unwrap());
366 }
367
368 #[tokio::test]
369 async fn test_delete_missing_file() {
370 let (storage, _temp_dir) = create_test_storage().await;
371
372 let result = storage.delete("missing_file.txt").await;
374 assert!(result.is_err());
375 assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
376 }
377
378 #[tokio::test]
379 async fn test_file_url() {
380 let storage = LocalStorage::new("/tmp/storage", "http://localhost/media");
381
382 assert_eq!(storage.url("test.file"), "http://localhost/media/test.file");
383
384 let storage2 = LocalStorage::new("/tmp/storage", "http://localhost/media/");
386 assert_eq!(
387 storage2.url("test.file"),
388 "http://localhost/media/test.file"
389 );
390 }
391
392 #[tokio::test]
393 async fn test_base_url() {
394 let storage = LocalStorage::new("/tmp/storage", "http://localhost/no_ending_slash");
396 assert_eq!(
397 storage.url("test.file"),
398 "http://localhost/no_ending_slash/test.file"
399 );
400 }
401
402 #[tokio::test]
403 async fn test_listdir() {
404 let (storage, _temp_dir) = create_test_storage().await;
405
406 storage
407 .save("storage_test_1", b"custom content")
408 .await
409 .unwrap();
410 storage
411 .save("storage_test_2", b"custom content")
412 .await
413 .unwrap();
414 storage.save("dir/file_c.txt", b"test").await.unwrap();
415
416 let files = storage.list("").await.unwrap();
417 let file_names: Vec<String> = files
418 .iter()
419 .map(|f| {
420 std::path::Path::new(&f.path)
421 .file_name()
422 .unwrap()
423 .to_string_lossy()
424 .to_string()
425 })
426 .collect();
427
428 assert!(file_names.contains(&"storage_test_1".to_string()));
429 assert!(file_names.contains(&"storage_test_2".to_string()));
430
431 storage.delete("storage_test_1").await.unwrap();
433 storage.delete("storage_test_2").await.unwrap();
434 storage.delete("dir/file_c.txt").await.unwrap();
435 }
436
437 #[tokio::test]
438 async fn test_open_missing_file() {
439 let (storage, _temp_dir) = create_test_storage().await;
440
441 let result = storage.read("missing.txt").await;
442 assert!(result.is_err());
443 assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
444 }
445
446 #[tokio::test]
447 async fn test_large_file_saving() {
448 let (storage, _temp_dir) = create_test_storage().await;
449
450 let large_content = vec![b'A'; 64 * 1024 * 3];
452 storage
453 .save("large_file.txt", &large_content)
454 .await
455 .unwrap();
456
457 let metadata = storage.metadata("large_file.txt").await.unwrap();
458 assert_eq!(metadata.size, large_content.len() as u64);
459
460 storage.delete("large_file.txt").await.unwrap();
461 }
462
463 #[tokio::test]
464 async fn test_file_checksum() {
465 let (storage, _temp_dir) = create_test_storage().await;
466
467 let metadata = storage.save("file.txt", b"test").await.unwrap();
468 assert!(metadata.checksum.is_some());
469
470 let metadata2 = storage.save("file2.txt", b"test").await.unwrap();
472 assert_eq!(metadata.checksum, metadata2.checksum);
473
474 storage.delete("file.txt").await.unwrap();
475 storage.delete("file2.txt").await.unwrap();
476 }
477
478 #[tokio::test]
479 async fn test_file_get_accessed_time() {
480 let (storage, _temp_dir) = create_test_storage().await;
481
482 storage.save("test.file", b"custom contents").await.unwrap();
483
484 let atime = storage.get_accessed_time("test.file").await.unwrap();
485 let now = chrono::Utc::now();
486
487 let diff = (now - atime).num_seconds().abs();
489 assert!(
490 diff < 5,
491 "Access time difference too large: {} seconds",
492 diff
493 );
494
495 storage.delete("test.file").await.unwrap();
496 }
497
498 #[tokio::test]
499 async fn test_file_get_created_time() {
500 let (storage, _temp_dir) = create_test_storage().await;
501
502 storage.save("test.file", b"custom contents").await.unwrap();
503
504 let ctime = storage.get_created_time("test.file").await.unwrap();
505 let now = chrono::Utc::now();
506
507 let diff = (now - ctime).num_seconds().abs();
509 assert!(
510 diff < 5,
511 "Creation time difference too large: {} seconds",
512 diff
513 );
514
515 storage.delete("test.file").await.unwrap();
516 }
517
518 #[tokio::test]
519 async fn test_file_get_modified_time() {
520 let (storage, _temp_dir) = create_test_storage().await;
521
522 storage.save("test.file", b"custom contents").await.unwrap();
523
524 let mtime = storage.get_modified_time("test.file").await.unwrap();
525 let now = chrono::Utc::now();
526
527 let diff = (now - mtime).num_seconds().abs();
529 assert!(
530 diff < 5,
531 "Modified time difference too large: {} seconds",
532 diff
533 );
534
535 storage.delete("test.file").await.unwrap();
536 }
537
538 #[tokio::test]
539 async fn test_file_modified_time_changes() {
540 use tokio::time::{Duration, sleep};
541
542 let (storage, _temp_dir) = create_test_storage().await;
543
544 storage.save("file.txt", b"test").await.unwrap();
545 let modified_time = storage.get_modified_time("file.txt").await.unwrap();
546
547 sleep(Duration::from_millis(100)).await;
549
550 storage.save("file.txt", b"new content").await.unwrap();
552
553 let new_modified_time = storage.get_modified_time("file.txt").await.unwrap();
554 assert!(
555 new_modified_time > modified_time,
556 "Modified time should increase after file change"
557 );
558
559 storage.delete("file.txt").await.unwrap();
560 }
561
562 #[tokio::test]
563 async fn test_file_storage_prevents_directory_traversal() {
564 let (storage, _temp_dir) = create_test_storage().await;
565
566 let result = storage.save("../test.txt", b"test").await;
568 assert!(result.is_err());
569 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
570
571 let result = storage.save("/etc/passwd", b"test").await;
573 assert!(result.is_err());
574 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
575 }
576
577 #[tokio::test]
578 async fn test_storage_dangerous_paths() {
579 let (storage, _temp_dir) = create_test_storage().await;
580
581 let dangerous_paths = vec!["..", ".", "", "../path", "tmp/../path", "/tmp/path"];
582
583 for path in dangerous_paths {
584 let result = storage.save(path, b"test").await;
585 assert!(
586 result.is_err(),
587 "Path '{}' should be rejected but was accepted",
588 path
589 );
590 assert!(
591 matches!(result.unwrap_err(), StorageError::InvalidPath(_)),
592 "Path '{}' should return InvalidPath error",
593 path
594 );
595 }
596 }
597
598 #[tokio::test]
599 async fn test_path_with_dots_in_filename() {
600 let (storage, _temp_dir) = create_test_storage().await;
601
602 storage.save("my.dir/test.file.txt", b"test").await.unwrap();
604 assert!(storage.exists("my.dir/test.file.txt").await.unwrap());
605
606 storage.delete("my.dir/test.file.txt").await.unwrap();
607 }
608
609 #[tokio::test]
610 async fn test_url_encoding() {
611 let storage = LocalStorage::new("/tmp/storage", "http://localhost/media");
612
613 assert_eq!(storage.url("test.file"), "http://localhost/media/test.file");
615
616 let url = storage.url("test file.txt");
619 assert!(url.contains("test file.txt"));
620 }
621
622 #[tokio::test]
623 async fn test_read_prevents_directory_traversal() {
624 let (storage, _temp_dir) = create_test_storage().await;
625
626 let result = storage.read("../test.txt").await;
628 assert!(result.is_err());
629 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
630
631 let result = storage.read("/etc/passwd").await;
633 assert!(result.is_err());
634 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
635
636 let result = storage.read("tmp/../test.txt").await;
638 assert!(result.is_err());
639 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
640 }
641
642 #[tokio::test]
643 async fn test_delete_prevents_directory_traversal() {
644 let (storage, _temp_dir) = create_test_storage().await;
645
646 let result = storage.delete("../test.txt").await;
648 assert!(result.is_err());
649 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
650
651 let result = storage.delete("/etc/passwd").await;
653 assert!(result.is_err());
654 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
655 }
656
657 #[tokio::test]
658 async fn test_exists_prevents_directory_traversal() {
659 let (storage, _temp_dir) = create_test_storage().await;
660
661 let result = storage.exists("../test.txt").await;
663 assert!(result.is_err());
664 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
665
666 let result = storage.exists("/etc/passwd").await;
668 assert!(result.is_err());
669 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
670 }
671
672 #[tokio::test]
673 async fn test_metadata_prevents_directory_traversal() {
674 let (storage, _temp_dir) = create_test_storage().await;
675
676 let result = storage.metadata("../test.txt").await;
678 assert!(result.is_err());
679 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
680
681 let result = storage.metadata("/etc/passwd").await;
683 assert!(result.is_err());
684 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
685 }
686
687 #[tokio::test]
688 async fn test_list_prevents_directory_traversal() {
689 let (storage, _temp_dir) = create_test_storage().await;
690
691 let result = storage.list("../test").await;
693 assert!(result.is_err());
694 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
695
696 let result = storage.list("/etc").await;
698 assert!(result.is_err());
699 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
700 }
701
702 #[tokio::test]
703 async fn test_get_time_operations_prevent_directory_traversal() {
704 let (storage, _temp_dir) = create_test_storage().await;
705
706 let result = storage.get_accessed_time("../test.txt").await;
708 assert!(result.is_err());
709 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
710
711 let result = storage.get_created_time("../test.txt").await;
713 assert!(result.is_err());
714 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
715
716 let result = storage.get_modified_time("../test.txt").await;
718 assert!(result.is_err());
719 assert!(matches!(result.unwrap_err(), StorageError::InvalidPath(_)));
720 }
721}