1use crate::spec_registry::FtpSpecRegistry;
2use crate::vfs::{VirtualFile, VirtualFileSystem};
3use async_trait::async_trait;
4use libunftp::storage::Result;
5use libunftp::storage::{Error, ErrorKind, Fileinfo, Metadata, StorageBackend};
6use std::fmt::Debug;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::time::SystemTime;
10
11#[derive(Debug, Clone)]
13pub struct MockForgeStorage {
14 vfs: Arc<VirtualFileSystem>,
15 spec_registry: Arc<FtpSpecRegistry>,
16}
17
18impl MockForgeStorage {
19 pub fn new(vfs: Arc<VirtualFileSystem>, spec_registry: Arc<FtpSpecRegistry>) -> Self {
20 Self { vfs, spec_registry }
21 }
22}
23
24#[async_trait]
25impl<U: libunftp::auth::UserDetail + Send + Sync + 'static> StorageBackend<U> for MockForgeStorage {
26 type Metadata = MockForgeMetadata;
27
28 async fn metadata<P: AsRef<Path> + Send + Debug>(
29 &self,
30 _user: &U,
31 path: P,
32 ) -> Result<Self::Metadata> {
33 let path = path.as_ref();
34
35 if let Some(file) = self.vfs.get_file(path) {
36 Ok(MockForgeMetadata {
37 file: Some(file),
38 is_dir: false,
39 })
40 } else {
41 if path == Path::new("/") || path == Path::new("") {
43 Ok(MockForgeMetadata {
44 file: None,
45 is_dir: true,
46 })
47 } else {
48 Err(Error::from(ErrorKind::PermanentFileNotAvailable))
49 }
50 }
51 }
52
53 async fn list<P: AsRef<Path> + Send + Debug>(
54 &self,
55 _user: &U,
56 path: P,
57 ) -> Result<Vec<Fileinfo<PathBuf, Self::Metadata>>> {
58 let path = path.as_ref();
59 let files = self.vfs.list_files(path);
60
61 let mut result = Vec::new();
62 for file in files {
63 result.push(Fileinfo {
64 path: file.path.clone(),
65 metadata: MockForgeMetadata {
66 file: Some(file),
67 is_dir: false,
68 },
69 });
70 }
71
72 Ok(result)
73 }
74
75 async fn get<P: AsRef<Path> + Send + Debug>(
76 &self,
77 _user: &U,
78 path: P,
79 _start_pos: u64,
80 ) -> Result<Box<dyn tokio::io::AsyncRead + Send + Sync + Unpin>> {
81 let path = path.as_ref();
82
83 if let Some(file) = self.vfs.get_file(path) {
84 let content =
85 file.render_content().map_err(|e| Error::new(ErrorKind::LocalError, e))?;
86 Ok(Box::new(std::io::Cursor::new(content)))
87 } else {
88 Err(Error::from(ErrorKind::PermanentFileNotAvailable))
89 }
90 }
91
92 async fn put<
93 P: AsRef<Path> + Send + Debug,
94 R: tokio::io::AsyncRead + Send + Sync + Unpin + 'static,
95 >(
96 &self,
97 _user: &U,
98 bytes: R,
99 path: P,
100 _start_pos: u64,
101 ) -> Result<u64> {
102 let path = path.as_ref();
103 let path_str = path.to_string_lossy().to_string();
104
105 use tokio::io::AsyncReadExt;
107 let mut data = Vec::new();
108 let mut reader = bytes;
109 reader
110 .read_to_end(&mut data)
111 .await
112 .map_err(|e| Error::new(ErrorKind::LocalError, e))?;
113
114 if let Some(rule) = self.spec_registry.find_upload_rule(&path_str) {
116 rule.validate_file(&data, &path_str)
118 .map_err(|e| Error::new(ErrorKind::PermissionDenied, e))?;
119
120 if rule.auto_accept {
121 let file = VirtualFile::new(
123 path.to_path_buf(),
124 crate::vfs::FileContent::Static(data.clone()),
125 crate::vfs::FileMetadata {
126 size: data.len() as u64,
127 ..Default::default()
128 },
129 );
130
131 self.vfs
132 .add_file(path.to_path_buf(), file)
133 .map_err(|e| Error::new(ErrorKind::LocalError, e))?;
134
135 let rule_name = Some(rule.path_pattern.clone());
137 self.spec_registry
138 .record_upload(path.to_path_buf(), data.len() as u64, rule_name)
139 .map_err(|e| Error::new(ErrorKind::LocalError, e))?;
140
141 Ok(data.len() as u64)
142 } else {
143 Err(Error::new(ErrorKind::PermissionDenied, "Upload rejected by rule"))
144 }
145 } else {
146 Err(Error::new(ErrorKind::PermissionDenied, "No upload rule matches this path"))
147 }
148 }
149
150 async fn del<P: AsRef<Path> + Send + Debug>(&self, _user: &U, path: P) -> Result<()> {
151 let path = path.as_ref();
152
153 if self.vfs.get_file(path).is_some() {
154 self.vfs.remove_file(path).map_err(|e| Error::new(ErrorKind::LocalError, e))?;
155 Ok(())
156 } else {
157 Err(Error::from(ErrorKind::PermanentFileNotAvailable))
158 }
159 }
160
161 async fn mkd<P: AsRef<Path> + Send + Debug>(&self, _user: &U, _path: P) -> Result<()> {
162 Err(Error::new(ErrorKind::PermissionDenied, "Directory creation not supported"))
164 }
165
166 async fn rename<P: AsRef<Path> + Send + Debug>(
167 &self,
168 _user: &U,
169 _from: P,
170 _to: P,
171 ) -> Result<()> {
172 Err(Error::new(ErrorKind::PermissionDenied, "Rename not supported"))
174 }
175
176 async fn rmd<P: AsRef<Path> + Send + Debug>(&self, _user: &U, _path: P) -> Result<()> {
177 Err(Error::new(ErrorKind::PermissionDenied, "Directory removal not supported"))
179 }
180
181 async fn cwd<P: AsRef<Path> + Send + Debug>(&self, _user: &U, _path: P) -> Result<()> {
182 Err(Error::new(ErrorKind::PermissionDenied, "Directory changes not supported"))
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct MockForgeMetadata {
190 file: Option<VirtualFile>,
191 is_dir: bool,
192}
193
194impl Metadata for MockForgeMetadata {
195 fn len(&self) -> u64 {
196 if let Some(file) = &self.file {
197 file.metadata.size
198 } else {
199 0
200 }
201 }
202
203 fn is_dir(&self) -> bool {
204 self.is_dir
205 }
206
207 fn is_file(&self) -> bool {
208 self.file.is_some()
209 }
210
211 fn is_symlink(&self) -> bool {
212 false
213 }
214
215 fn modified(&self) -> Result<SystemTime> {
216 if let Some(file) = &self.file {
217 Ok(SystemTime::UNIX_EPOCH
219 + std::time::Duration::from_secs(file.modified_at.timestamp() as u64))
220 } else {
221 Ok(SystemTime::now())
222 }
223 }
224
225 fn gid(&self) -> u32 {
226 1000 }
228
229 fn uid(&self) -> u32 {
230 1000 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::fixtures::{
238 FileContentConfig, FileValidation, FtpFixture, UploadRule, UploadStorage, VirtualFileConfig,
239 };
240 use crate::vfs::{FileContent, FileMetadata};
241
242 #[test]
243 fn test_mockforge_storage_new() {
244 let vfs = Arc::new(VirtualFileSystem::new(std::path::PathBuf::from("/")));
245 let spec_registry = Arc::new(FtpSpecRegistry::new());
246 let storage = MockForgeStorage::new(vfs.clone(), spec_registry.clone());
247
248 let debug = format!("{:?}", storage);
249 assert!(debug.contains("MockForgeStorage"));
250 }
251
252 #[test]
253 fn test_mockforge_storage_clone() {
254 let vfs = Arc::new(VirtualFileSystem::new(std::path::PathBuf::from("/")));
255 let spec_registry = Arc::new(FtpSpecRegistry::new());
256 let storage = MockForgeStorage::new(vfs.clone(), spec_registry.clone());
257
258 let _cloned = storage.clone();
259 }
261
262 #[test]
263 fn test_mockforge_metadata_len_with_file() {
264 let file = VirtualFile::new(
265 std::path::PathBuf::from("/test.txt"),
266 FileContent::Static(b"test content".to_vec()),
267 FileMetadata {
268 size: 1024,
269 ..Default::default()
270 },
271 );
272
273 let metadata = MockForgeMetadata {
274 file: Some(file),
275 is_dir: false,
276 };
277
278 assert_eq!(metadata.len(), 1024);
279 }
280
281 #[test]
282 fn test_mockforge_metadata_len_without_file() {
283 let metadata = MockForgeMetadata {
284 file: None,
285 is_dir: true,
286 };
287
288 assert_eq!(metadata.len(), 0);
289 }
290
291 #[test]
292 fn test_mockforge_metadata_is_dir() {
293 let metadata = MockForgeMetadata {
294 file: None,
295 is_dir: true,
296 };
297
298 assert!(metadata.is_dir());
299 assert!(!metadata.is_file());
300 }
301
302 #[test]
303 fn test_mockforge_metadata_is_file() {
304 let file = VirtualFile::new(
305 std::path::PathBuf::from("/test.txt"),
306 FileContent::Static(vec![]),
307 FileMetadata::default(),
308 );
309
310 let metadata = MockForgeMetadata {
311 file: Some(file),
312 is_dir: false,
313 };
314
315 assert!(metadata.is_file());
316 assert!(!metadata.is_dir());
317 }
318
319 #[test]
320 fn test_mockforge_metadata_is_symlink() {
321 let metadata = MockForgeMetadata {
322 file: None,
323 is_dir: false,
324 };
325
326 assert!(!metadata.is_symlink());
327 }
328
329 #[test]
330 fn test_mockforge_metadata_modified() {
331 let file = VirtualFile::new(
332 std::path::PathBuf::from("/test.txt"),
333 FileContent::Static(vec![]),
334 FileMetadata::default(),
335 );
336
337 let metadata = MockForgeMetadata {
338 file: Some(file),
339 is_dir: false,
340 };
341
342 let modified = metadata.modified();
343 assert!(modified.is_ok());
344 }
345
346 #[test]
347 fn test_mockforge_metadata_modified_no_file() {
348 let metadata = MockForgeMetadata {
349 file: None,
350 is_dir: true,
351 };
352
353 let modified = metadata.modified();
354 assert!(modified.is_ok());
355 }
356
357 #[test]
358 fn test_mockforge_metadata_gid() {
359 let metadata = MockForgeMetadata {
360 file: None,
361 is_dir: false,
362 };
363
364 assert_eq!(metadata.gid(), 1000);
365 }
366
367 #[test]
368 fn test_mockforge_metadata_uid() {
369 let metadata = MockForgeMetadata {
370 file: None,
371 is_dir: false,
372 };
373
374 assert_eq!(metadata.uid(), 1000);
375 }
376
377 #[test]
378 fn test_mockforge_metadata_clone() {
379 let file = VirtualFile::new(
380 std::path::PathBuf::from("/test.txt"),
381 FileContent::Static(vec![]),
382 FileMetadata::default(),
383 );
384
385 let metadata = MockForgeMetadata {
386 file: Some(file),
387 is_dir: false,
388 };
389
390 let _cloned = metadata.clone();
391 }
393
394 #[test]
395 fn test_mockforge_metadata_debug() {
396 let metadata = MockForgeMetadata {
397 file: None,
398 is_dir: true,
399 };
400
401 let debug = format!("{:?}", metadata);
402 assert!(debug.contains("MockForgeMetadata"));
403 }
404
405 }