modo_upload/storage/
local.rs1use super::{FileStorage, StoredFile, ensure_within, generate_filename};
2use crate::file::UploadedFile;
3use crate::stream::BufferedUpload;
4use std::path::{Path, PathBuf};
5use tokio::io::AsyncWriteExt;
6
7pub struct LocalStorage {
15 base_dir: PathBuf,
16}
17
18impl LocalStorage {
19 pub fn new(base_dir: impl Into<PathBuf>) -> Self {
24 Self {
25 base_dir: base_dir.into(),
26 }
27 }
28}
29
30#[async_trait::async_trait]
31impl FileStorage for LocalStorage {
32 async fn store(&self, prefix: &str, file: &UploadedFile) -> Result<StoredFile, modo::Error> {
33 let filename = generate_filename(file.file_name());
34 let rel_path = format!("{prefix}/{filename}");
35 let full_path = ensure_within(&self.base_dir, Path::new(&rel_path))?;
36
37 if let Some(parent) = full_path.parent() {
38 tokio::fs::create_dir_all(parent)
39 .await
40 .map_err(|e| modo::Error::internal(format!("Failed to create directory: {e}")))?;
41 }
42
43 tokio::fs::write(&full_path, file.data())
44 .await
45 .map_err(|e| modo::Error::internal(format!("Failed to write file: {e}")))?;
46
47 Ok(StoredFile {
48 path: rel_path,
49 size: file.size() as u64,
50 })
51 }
52
53 async fn store_stream(
54 &self,
55 prefix: &str,
56 stream: &mut BufferedUpload,
57 ) -> Result<StoredFile, modo::Error> {
58 let filename = generate_filename(stream.file_name());
59 let rel_path = format!("{prefix}/{filename}");
60 let full_path = ensure_within(&self.base_dir, Path::new(&rel_path))?;
61
62 if let Some(parent) = full_path.parent() {
63 tokio::fs::create_dir_all(parent)
64 .await
65 .map_err(|e| modo::Error::internal(format!("Failed to create directory: {e}")))?;
66 }
67
68 let mut file = tokio::fs::File::create(&full_path)
69 .await
70 .map_err(|e| modo::Error::internal(format!("Failed to create file: {e}")))?;
71
72 let mut total_size: u64 = 0;
73 while let Some(chunk) = stream.chunk().await {
74 let chunk =
75 chunk.map_err(|e| modo::Error::internal(format!("Failed to read chunk: {e}")))?;
76 total_size += chunk.len() as u64;
77 file.write_all(&chunk)
78 .await
79 .map_err(|e| modo::Error::internal(format!("Failed to write chunk: {e}")))?;
80 }
81 file.flush()
82 .await
83 .map_err(|e| modo::Error::internal(format!("Failed to flush file: {e}")))?;
84
85 Ok(StoredFile {
86 path: rel_path,
87 size: total_size,
88 })
89 }
90
91 async fn delete(&self, path: &str) -> Result<(), modo::Error> {
92 let full_path = ensure_within(&self.base_dir, Path::new(path))?;
93 tokio::fs::remove_file(&full_path)
94 .await
95 .map_err(|e| modo::Error::internal(format!("Failed to delete file: {e}")))?;
96 Ok(())
97 }
98
99 async fn exists(&self, path: &str) -> Result<bool, modo::Error> {
100 let full_path = ensure_within(&self.base_dir, Path::new(path))?;
101 Ok(tokio::fs::try_exists(&full_path)
102 .await
103 .map_err(|e| modo::Error::internal(format!("Failed to check file: {e}")))?)
104 }
105}