Skip to main content

sz_orm_storage/
local.rs

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 temp_dir() -> std::path::PathBuf {
64        let nanos = std::time::SystemTime::now()
65            .duration_since(std::time::UNIX_EPOCH)
66            .unwrap_or_default()
67            .as_nanos();
68        std::env::temp_dir().join(format!("local_storage_test_{:x}", nanos))
69    }
70
71    #[tokio::test]
72    async fn test_local_put_and_get() {
73        let dir = temp_dir();
74        let storage = LocalStorage::new(dir.to_string_lossy());
75
76        storage
77            .put("file.txt", b"hello", "text/plain")
78            .await
79            .unwrap();
80        let data = storage.get("file.txt").await.unwrap();
81        assert_eq!(data, b"hello");
82
83        tokio::fs::remove_dir_all(&dir).await.ok();
84    }
85
86    #[tokio::test]
87    async fn test_local_get_not_found() {
88        let dir = temp_dir();
89        let storage = LocalStorage::new(dir.to_string_lossy());
90
91        let result = storage.get("missing.txt").await;
92        assert!(result.is_err());
93        assert!(matches!(result.unwrap_err(), StorageError::NotFound(_)));
94
95        tokio::fs::remove_dir_all(&dir).await.ok();
96    }
97
98    #[tokio::test]
99    async fn test_local_delete() {
100        let dir = temp_dir();
101        let storage = LocalStorage::new(dir.to_string_lossy());
102
103        storage
104            .put("delete.txt", b"data", "text/plain")
105            .await
106            .unwrap();
107        assert!(storage.exists("delete.txt").await.unwrap());
108
109        storage.delete("delete.txt").await.unwrap();
110        assert!(!storage.exists("delete.txt").await.unwrap());
111
112        tokio::fs::remove_dir_all(&dir).await.ok();
113    }
114
115    #[tokio::test]
116    async fn test_local_exists_false_for_missing() {
117        let dir = temp_dir();
118        let storage = LocalStorage::new(dir.to_string_lossy());
119        assert!(!storage.exists("nope.txt").await.unwrap());
120        tokio::fs::remove_dir_all(&dir).await.ok();
121    }
122
123    #[tokio::test]
124    async fn test_local_creates_subdirectories() {
125        let dir = temp_dir();
126        let storage = LocalStorage::new(dir.to_string_lossy());
127
128        storage
129            .put("nested/deep/file.txt", b"nested", "text/plain")
130            .await
131            .unwrap();
132        let data = storage.get("nested/deep/file.txt").await.unwrap();
133        assert_eq!(data, b"nested");
134
135        tokio::fs::remove_dir_all(&dir).await.ok();
136    }
137
138    #[tokio::test]
139    async fn test_local_put_returns_url() {
140        let dir = temp_dir();
141        let storage = LocalStorage::new(dir.to_string_lossy());
142
143        let url = storage.put("url.txt", b"data", "text/plain").await.unwrap();
144        assert!(url.starts_with("local://"));
145        assert!(url.contains("url.txt"));
146
147        tokio::fs::remove_dir_all(&dir).await.ok();
148    }
149
150    #[tokio::test]
151    async fn test_local_overwrite() {
152        let dir = temp_dir();
153        let storage = LocalStorage::new(dir.to_string_lossy());
154
155        storage
156            .put("overwrite.txt", b"v1", "text/plain")
157            .await
158            .unwrap();
159        storage
160            .put("overwrite.txt", b"v2", "text/plain")
161            .await
162            .unwrap();
163        let data = storage.get("overwrite.txt").await.unwrap();
164        assert_eq!(data, b"v2");
165
166        tokio::fs::remove_dir_all(&dir).await.ok();
167    }
168}