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_async(path).await {
36 Ok(MockForgeMetadata {
37 file: Some(file),
38 is_dir: false,
39 })
40 } else if self.vfs.directory_exists_async(path).await {
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_async(path).await;
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_async(path).await {
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_async(path.to_path_buf(), file)
130 .await
131 .map_err(|e| Error::new(ErrorKind::LocalError, e))?;
132
133 let rule_name = Some(rule.path_pattern.clone());
135 self.spec_registry
136 .record_upload(path.to_path_buf(), data.len() as u64, rule_name)
137 .map_err(|e| Error::new(ErrorKind::LocalError, e))?;
138
139 Ok(data.len() as u64)
140 } else {
141 Err(Error::new(ErrorKind::PermissionDenied, "Upload rejected by rule"))
142 }
143 } else {
144 Err(Error::new(ErrorKind::PermissionDenied, "No upload rule matches this path"))
145 }
146 }
147
148 async fn del<P: AsRef<Path> + Send + Debug>(&self, _user: &U, path: P) -> Result<()> {
149 let path = path.as_ref();
150
151 if self.vfs.get_file_async(path).await.is_some() {
152 self.vfs
153 .remove_file_async(path)
154 .await
155 .map_err(|e| Error::new(ErrorKind::LocalError, e))?;
156 Ok(())
157 } else {
158 Err(Error::from(ErrorKind::PermanentFileNotAvailable))
159 }
160 }
161
162 async fn mkd<P: AsRef<Path> + Send + Debug>(&self, _user: &U, path: P) -> Result<()> {
163 let path = path.as_ref();
164 if self.vfs.directory_exists_async(path).await {
165 return Err(Error::new(
166 ErrorKind::PermanentFileNotAvailable,
167 "Directory already exists",
168 ));
169 }
170 self.vfs
171 .create_directory_async(path.to_path_buf())
172 .await
173 .map_err(|e| Error::new(ErrorKind::LocalError, e))
174 }
175
176 async fn rename<P: AsRef<Path> + Send + Debug>(&self, _user: &U, from: P, to: P) -> Result<()> {
177 let from = from.as_ref();
178 let to = to.as_ref();
179
180 if let Some(file) = self.vfs.get_file_async(from).await {
181 self.vfs
182 .remove_file_async(from)
183 .await
184 .map_err(|e| Error::new(ErrorKind::LocalError, e))?;
185 self.vfs
186 .add_file_async(
187 to.to_path_buf(),
188 VirtualFile::new(to.to_path_buf(), file.content, file.metadata),
189 )
190 .await
191 .map_err(|e| Error::new(ErrorKind::LocalError, e))
192 } else {
193 Err(Error::from(ErrorKind::PermanentFileNotAvailable))
194 }
195 }
196
197 async fn rmd<P: AsRef<Path> + Send + Debug>(&self, _user: &U, path: P) -> Result<()> {
198 let path = path.as_ref();
199 if !self.vfs.directory_exists_async(path).await {
200 return Err(Error::from(ErrorKind::PermanentFileNotAvailable));
201 }
202 self.vfs
203 .remove_directory_async(path)
204 .await
205 .map_err(|e| Error::new(ErrorKind::PermissionDenied, e))
206 }
207
208 async fn cwd<P: AsRef<Path> + Send + Debug>(&self, _user: &U, path: P) -> Result<()> {
209 let path = path.as_ref();
210 if self.vfs.directory_exists_async(path).await {
211 Ok(())
212 } else {
213 Err(Error::from(ErrorKind::PermanentFileNotAvailable))
214 }
215 }
216}
217
218#[derive(Debug, Clone)]
220pub struct MockForgeMetadata {
221 file: Option<VirtualFile>,
222 is_dir: bool,
223}
224
225impl Metadata for MockForgeMetadata {
226 fn len(&self) -> u64 {
227 if let Some(file) = &self.file {
228 file.metadata.size
229 } else {
230 0
231 }
232 }
233
234 fn is_dir(&self) -> bool {
235 self.is_dir
236 }
237
238 fn is_file(&self) -> bool {
239 self.file.is_some()
240 }
241
242 fn is_symlink(&self) -> bool {
243 false
244 }
245
246 fn modified(&self) -> Result<SystemTime> {
247 if let Some(file) = &self.file {
248 Ok(SystemTime::UNIX_EPOCH
250 + std::time::Duration::from_secs(file.modified_at.timestamp() as u64))
251 } else {
252 Ok(SystemTime::now())
253 }
254 }
255
256 fn gid(&self) -> u32 {
257 1000 }
259
260 fn uid(&self) -> u32 {
261 1000 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::vfs::{FileContent, FileMetadata};
269
270 #[test]
271 fn test_mockforge_storage_new() {
272 let vfs = Arc::new(VirtualFileSystem::new(PathBuf::from("/")));
273 let spec_registry = Arc::new(FtpSpecRegistry::new());
274 let storage = MockForgeStorage::new(vfs.clone(), spec_registry.clone());
275
276 let debug = format!("{:?}", storage);
277 assert!(debug.contains("MockForgeStorage"));
278 }
279
280 #[test]
281 fn test_mockforge_storage_clone() {
282 let vfs = Arc::new(VirtualFileSystem::new(PathBuf::from("/")));
283 let spec_registry = Arc::new(FtpSpecRegistry::new());
284 let storage = MockForgeStorage::new(vfs.clone(), spec_registry.clone());
285
286 let _cloned = storage.clone();
287 }
289
290 #[test]
291 fn test_mockforge_metadata_len_with_file() {
292 let file = VirtualFile::new(
293 PathBuf::from("/test.txt"),
294 FileContent::Static(b"test content".to_vec()),
295 FileMetadata {
296 size: 1024,
297 ..Default::default()
298 },
299 );
300
301 let metadata = MockForgeMetadata {
302 file: Some(file),
303 is_dir: false,
304 };
305
306 assert_eq!(metadata.len(), 1024);
307 }
308
309 #[test]
310 fn test_mockforge_metadata_len_without_file() {
311 let metadata = MockForgeMetadata {
312 file: None,
313 is_dir: true,
314 };
315
316 assert_eq!(metadata.len(), 0);
317 }
318
319 #[test]
320 fn test_mockforge_metadata_is_dir() {
321 let metadata = MockForgeMetadata {
322 file: None,
323 is_dir: true,
324 };
325
326 assert!(metadata.is_dir());
327 assert!(!metadata.is_file());
328 }
329
330 #[test]
331 fn test_mockforge_metadata_is_file() {
332 let file = VirtualFile::new(
333 PathBuf::from("/test.txt"),
334 FileContent::Static(vec![]),
335 FileMetadata::default(),
336 );
337
338 let metadata = MockForgeMetadata {
339 file: Some(file),
340 is_dir: false,
341 };
342
343 assert!(metadata.is_file());
344 assert!(!metadata.is_dir());
345 }
346
347 #[test]
348 fn test_mockforge_metadata_is_symlink() {
349 let metadata = MockForgeMetadata {
350 file: None,
351 is_dir: false,
352 };
353
354 assert!(!metadata.is_symlink());
355 }
356
357 #[test]
358 fn test_mockforge_metadata_modified() {
359 let file = VirtualFile::new(
360 PathBuf::from("/test.txt"),
361 FileContent::Static(vec![]),
362 FileMetadata::default(),
363 );
364
365 let metadata = MockForgeMetadata {
366 file: Some(file),
367 is_dir: false,
368 };
369
370 let modified = metadata.modified();
371 assert!(modified.is_ok());
372 }
373
374 #[test]
375 fn test_mockforge_metadata_modified_no_file() {
376 let metadata = MockForgeMetadata {
377 file: None,
378 is_dir: true,
379 };
380
381 let modified = metadata.modified();
382 assert!(modified.is_ok());
383 }
384
385 #[test]
386 fn test_mockforge_metadata_gid() {
387 let metadata = MockForgeMetadata {
388 file: None,
389 is_dir: false,
390 };
391
392 assert_eq!(metadata.gid(), 1000);
393 }
394
395 #[test]
396 fn test_mockforge_metadata_uid() {
397 let metadata = MockForgeMetadata {
398 file: None,
399 is_dir: false,
400 };
401
402 assert_eq!(metadata.uid(), 1000);
403 }
404
405 #[test]
406 fn test_mockforge_metadata_clone() {
407 let file = VirtualFile::new(
408 PathBuf::from("/test.txt"),
409 FileContent::Static(vec![]),
410 FileMetadata::default(),
411 );
412
413 let metadata = MockForgeMetadata {
414 file: Some(file),
415 is_dir: false,
416 };
417
418 let _cloned = metadata.clone();
419 }
421
422 #[test]
423 fn test_mockforge_metadata_debug() {
424 let metadata = MockForgeMetadata {
425 file: None,
426 is_dir: true,
427 };
428
429 let debug = format!("{:?}", metadata);
430 assert!(debug.contains("MockForgeMetadata"));
431 }
432
433 }