Skip to main content

modo_upload/storage/
local.rs

1use 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
8/// Local filesystem storage backend.
9///
10/// Files are written under `base_dir/<prefix>/<ulid>.<ext>`.  Path traversal
11/// is rejected at the storage layer — `..` components and absolute paths
12/// return an error before any filesystem operation is attempted.
13///
14/// Requires the `local` feature (enabled by default).
15pub struct LocalStorage {
16    base_dir: PathBuf,
17}
18
19impl LocalStorage {
20    /// Create a new `LocalStorage` rooted at `base_dir`.
21    ///
22    /// The directory does not need to exist at construction time; it is
23    /// created on the first [`store`](crate::FileStorageDyn::store) call.
24    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}