mockforge_ftp/
storage.rs

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/// Custom storage backend for libunftp that integrates with MockForge's VFS
12#[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            // Check if it's a directory (for now, assume root is a directory)
42            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        // Read all data
106        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        // Check upload rules
115        if let Some(rule) = self.spec_registry.find_upload_rule(&path_str) {
116            // Validate the upload
117            rule.validate_file(&data, &path_str)
118                .map_err(|e| Error::new(ErrorKind::PermissionDenied, e))?;
119
120            if rule.auto_accept {
121                // Store the file
122                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                // Record the upload
136                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        // For now, directories are not supported in VFS
163        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        // For now, renaming is not supported
173        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        // For now, directory removal is not supported
178        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        // For now, directory changes are not supported
183        Err(Error::new(ErrorKind::PermissionDenied, "Directory changes not supported"))
184    }
185}
186
187/// Metadata implementation for MockForge storage
188#[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            // Convert DateTime to SystemTime (approximate)
218            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 // Default GID
227    }
228
229    fn uid(&self) -> u32 {
230        1000 // Default UID
231    }
232}