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