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