1use crate::error::StorageError;
2use crate::storage::Storage;
3use async_trait::async_trait;
4use std::path::PathBuf;
5
6pub struct LocalStorage {
7 pub base_path: String,
8}
9
10impl LocalStorage {
11 pub fn new(base_path: impl Into<String>) -> Self {
12 Self {
13 base_path: base_path.into(),
14 }
15 }
16
17 pub fn full_path(&self, key: &str) -> PathBuf {
18 PathBuf::from(&self.base_path).join(key)
19 }
20}
21
22#[async_trait]
23impl Storage for LocalStorage {
24 async fn put(
25 &self,
26 key: &str,
27 data: &[u8],
28 _content_type: &str,
29 ) -> Result<String, StorageError> {
30 let path = self.full_path(key);
31 if let Some(parent) = path.parent() {
32 tokio::fs::create_dir_all(parent).await?;
33 }
34 tokio::fs::write(&path, data).await?;
35 Ok(format!("local://{}", key))
36 }
37
38 async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
39 let path = self.full_path(key);
40 if !path.exists() {
41 return Err(StorageError::NotFound(key.to_string()));
42 }
43 tokio::fs::read(&path).await.map_err(StorageError::from)
44 }
45
46 async fn delete(&self, key: &str) -> Result<(), StorageError> {
47 let path = self.full_path(key);
48 if path.exists() {
49 tokio::fs::remove_file(&path).await?;
50 }
51 Ok(())
52 }
53
54 async fn exists(&self, key: &str) -> Result<bool, StorageError> {
55 Ok(self.full_path(key).exists())
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 fn test_data_base() -> std::path::PathBuf {
68 let f_drive = std::path::Path::new("F:\\test\\data");
69 if is_dir_writable(f_drive) {
70 return f_drive.to_path_buf();
71 }
72 if let Ok(dir) = std::env::var("SZ_ORM_TEST_DATA_DIR") {
73 let p = std::path::PathBuf::from(&dir);
74 if is_dir_writable(&p) {
75 return p;
76 }
77 }
78 std::env::temp_dir()
79 }
80
81 fn is_dir_writable(dir: &std::path::Path) -> bool {
83 if !dir.exists() {
84 return false;
85 }
86 let probe = dir.join(format!(".probe_{}", std::process::id()));
87 match std::fs::File::create(&probe) {
88 Ok(_) => {
89 let _ = std::fs::remove_file(&probe);
90 true
91 }
92 Err(_) => false,
93 }
94 }
95
96 fn temp_dir() -> std::path::PathBuf {
97 let nanos = std::time::SystemTime::now()
98 .duration_since(std::time::UNIX_EPOCH)
99 .unwrap_or_default()
100 .as_nanos();
101 test_data_base().join(format!("local_storage_test_{:x}", nanos))
102 }
103
104 #[tokio::test]
105 async fn test_local_put_and_get() {
106 let dir = temp_dir();
107 let storage = LocalStorage::new(dir.to_string_lossy());
108
109 storage
110 .put("file.txt", b"hello", "text/plain")
111 .await
112 .unwrap();
113 let data = storage.get("file.txt").await.unwrap();
114 assert_eq!(data, b"hello");
115
116 tokio::fs::remove_dir_all(&dir).await.ok();
117 }
118
119 #[tokio::test]
120 async fn test_local_get_not_found() {
121 let dir = temp_dir();
122 let storage = LocalStorage::new(dir.to_string_lossy());
123
124 let result = storage.get("missing.txt").await;
125 assert!(result.is_err());
126 assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
127
128 tokio::fs::remove_dir_all(&dir).await.ok();
129 }
130
131 #[tokio::test]
132 async fn test_local_delete() {
133 let dir = temp_dir();
134 let storage = LocalStorage::new(dir.to_string_lossy());
135
136 storage
137 .put("delete.txt", b"data", "text/plain")
138 .await
139 .unwrap();
140 assert!(storage.exists("delete.txt").await.unwrap());
141
142 storage.delete("delete.txt").await.unwrap();
143 assert!(!storage.exists("delete.txt").await.unwrap());
144
145 tokio::fs::remove_dir_all(&dir).await.ok();
146 }
147
148 #[tokio::test]
149 async fn test_local_exists_false_for_missing() {
150 let dir = temp_dir();
151 let storage = LocalStorage::new(dir.to_string_lossy());
152 assert!(!storage.exists("nope.txt").await.unwrap());
153 tokio::fs::remove_dir_all(&dir).await.ok();
154 }
155
156 #[tokio::test]
157 async fn test_local_creates_subdirectories() {
158 let dir = temp_dir();
159 let storage = LocalStorage::new(dir.to_string_lossy());
160
161 storage
162 .put("nested/deep/file.txt", b"nested", "text/plain")
163 .await
164 .unwrap();
165 let data = storage.get("nested/deep/file.txt").await.unwrap();
166 assert_eq!(data, b"nested");
167
168 tokio::fs::remove_dir_all(&dir).await.ok();
169 }
170
171 #[tokio::test]
172 async fn test_local_put_returns_url() {
173 let dir = temp_dir();
174 let storage = LocalStorage::new(dir.to_string_lossy());
175
176 let url = storage.put("url.txt", b"data", "text/plain").await.unwrap();
177 assert!(url.starts_with("local://"));
178 assert!(url.contains("url.txt"));
179
180 tokio::fs::remove_dir_all(&dir).await.ok();
181 }
182
183 #[tokio::test]
184 async fn test_local_overwrite() {
185 let dir = temp_dir();
186 let storage = LocalStorage::new(dir.to_string_lossy());
187
188 storage
189 .put("overwrite.txt", b"v1", "text/plain")
190 .await
191 .unwrap();
192 storage
193 .put("overwrite.txt", b"v2", "text/plain")
194 .await
195 .unwrap();
196 let data = storage.get("overwrite.txt").await.unwrap();
197 assert_eq!(data, b"v2");
198
199 tokio::fs::remove_dir_all(&dir).await.ok();
200 }
201}