rws_storage/storage/
local.rs1use super::{Storage, StorageError};
2use std::fs;
3use std::path::PathBuf;
4
5pub struct LocalStorage {
17 root: PathBuf,
18 base_url: String,
19}
20
21impl LocalStorage {
22 pub fn new(root: impl Into<String>) -> Self {
25 LocalStorage { root: PathBuf::from(root.into()), base_url: String::new() }
26 }
27
28 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
32 self.base_url = base_url.into();
33 self
34 }
35
36 fn resolve(&self, key: &str) -> Result<PathBuf, StorageError> {
37 if key.split('/').any(|segment| segment == "..") {
38 return Err(StorageError::new(format!("key '{key}' must not contain '..' segments")));
39 }
40 Ok(self.root.join(key.trim_start_matches('/')))
41 }
42}
43
44impl Storage for LocalStorage {
45 fn put(&self, key: &str, data: &[u8], _content_type: &str) -> Result<String, StorageError> {
46 let path = self.resolve(key)?;
47 if let Some(parent) = path.parent() {
48 fs::create_dir_all(parent)
49 .map_err(|e| StorageError::new(format!("failed to create '{}': {e}", parent.display())))?;
50 }
51 fs::write(&path, data).map_err(|e| StorageError::new(format!("failed to write '{key}': {e}")))?;
52 Ok(key.to_string())
53 }
54
55 fn put_stream(&self, key: &str, reader: &mut dyn std::io::Read, _content_type: &str) -> Result<String, StorageError> {
59 let path = self.resolve(key)?;
60 if let Some(parent) = path.parent() {
61 fs::create_dir_all(parent)
62 .map_err(|e| StorageError::new(format!("failed to create '{}': {e}", parent.display())))?;
63 }
64 let partial = path.with_file_name(format!(
65 ".{}.partial-{}",
66 path.file_name().and_then(|n| n.to_str()).unwrap_or("upload"),
67 std::process::id()
68 ));
69 let written = fs::File::create(&partial).and_then(|mut file| {
70 std::io::copy(reader, &mut file)?;
71 file.sync_all()
72 });
73 if let Err(e) = written.and_then(|_| fs::rename(&partial, &path)) {
74 let _ = fs::remove_file(&partial);
75 return Err(StorageError::new(format!("failed to write '{key}': {e}")));
76 }
77 Ok(key.to_string())
78 }
79
80 fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
81 let path = self.resolve(key)?;
82 fs::read(&path).map_err(|e| StorageError::new(format!("failed to read '{key}': {e}")))
83 }
84
85 fn delete(&self, key: &str) -> Result<(), StorageError> {
86 let path = self.resolve(key)?;
87 match fs::remove_file(&path) {
88 Ok(()) => Ok(()),
89 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
90 Err(e) => Err(StorageError::new(format!("failed to delete '{key}': {e}"))),
91 }
92 }
93
94 fn url(&self, key: &str) -> String {
95 if self.base_url.is_empty() {
96 match self.resolve(key) {
97 Ok(path) => path.to_string_lossy().to_string(),
98 Err(_) => key.to_string(),
99 }
100 } else {
101 format!("{}/{}", self.base_url.trim_end_matches('/'), key.trim_start_matches('/'))
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests;