mockforge_ftp/
vfs.rs

1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use handlebars::Handlebars;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// Virtual File System for FTP server
12#[derive(Debug, Clone)]
13pub struct VirtualFileSystem {
14    #[allow(dead_code)]
15    root: PathBuf,
16    files: Arc<RwLock<HashMap<PathBuf, VirtualFile>>>,
17    fixtures: Arc<RwLock<HashMap<PathBuf, FileFixture>>>,
18}
19
20impl VirtualFileSystem {
21    pub fn new(root: PathBuf) -> Self {
22        Self {
23            root,
24            files: Arc::new(RwLock::new(HashMap::new())),
25            fixtures: Arc::new(RwLock::new(HashMap::new())),
26        }
27    }
28
29    pub fn add_file(&self, path: PathBuf, file: VirtualFile) -> Result<()> {
30        let mut files = self.files.blocking_write();
31        files.insert(path, file);
32        Ok(())
33    }
34
35    pub fn get_file(&self, path: &Path) -> Option<VirtualFile> {
36        let files = self.files.blocking_read();
37        if let Some(file) = files.get(path) {
38            return Some(file.clone());
39        }
40
41        // Check fixtures
42        let fixtures = self.fixtures.blocking_read();
43        if let Some(fixture) = fixtures.get(path) {
44            return Some(fixture.clone().to_virtual_file());
45        }
46
47        None
48    }
49
50    pub fn remove_file(&self, path: &Path) -> Result<()> {
51        let mut files = self.files.blocking_write();
52        files.remove(path);
53        Ok(())
54    }
55
56    pub fn list_files(&self, path: &Path) -> Vec<VirtualFile> {
57        let files = self.files.blocking_read();
58        files
59            .iter()
60            .filter(|(file_path, _)| file_path.starts_with(path))
61            .map(|(_, file)| file.clone())
62            .collect()
63    }
64
65    pub fn clear(&self) -> Result<()> {
66        let mut files = self.files.blocking_write();
67        files.clear();
68        Ok(())
69    }
70
71    pub fn add_fixture(&self, fixture: FileFixture) -> Result<()> {
72        let mut fixtures = self.fixtures.blocking_write();
73        fixtures.insert(fixture.path.clone(), fixture);
74        Ok(())
75    }
76
77    pub fn load_fixtures(&self, fixtures: Vec<FileFixture>) -> Result<()> {
78        for fixture in fixtures {
79            self.add_fixture(fixture)?;
80        }
81        Ok(())
82    }
83}
84
85/// Virtual file representation
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct VirtualFile {
88    pub path: PathBuf,
89    pub content: FileContent,
90    pub metadata: FileMetadata,
91    pub created_at: DateTime<Utc>,
92    pub modified_at: DateTime<Utc>,
93}
94
95impl VirtualFile {
96    pub fn new(path: PathBuf, content: FileContent, metadata: FileMetadata) -> Self {
97        let now = Utc::now();
98        Self {
99            path,
100            content,
101            metadata,
102            created_at: now,
103            modified_at: now,
104        }
105    }
106
107    pub fn render_content(&self) -> Result<Vec<u8>> {
108        match &self.content {
109            FileContent::Static(data) => Ok(data.clone()),
110            FileContent::Template(template) => {
111                // Render template using Handlebars
112                let handlebars = Handlebars::new();
113                let context = create_template_context();
114                let rendered = handlebars.render_template(template, &context)?;
115                Ok(rendered.into_bytes())
116            }
117            FileContent::Generated { size, pattern } => match pattern {
118                GenerationPattern::Random => Ok((0..*size).map(|_| rand::random::<u8>()).collect()),
119                GenerationPattern::Zeros => Ok(vec![0; *size]),
120                GenerationPattern::Ones => Ok(vec![1; *size]),
121                GenerationPattern::Incremental => Ok((0..*size).map(|i| (i % 256) as u8).collect()),
122            },
123        }
124    }
125}
126
127/// File content types
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub enum FileContent {
130    Static(Vec<u8>),
131    Template(String),
132    Generated {
133        size: usize,
134        pattern: GenerationPattern,
135    },
136}
137
138/// File metadata
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct FileMetadata {
141    pub permissions: String,
142    pub owner: String,
143    pub group: String,
144    pub size: u64,
145}
146
147impl Default for FileMetadata {
148    fn default() -> Self {
149        Self {
150            permissions: "644".to_string(),
151            owner: "mockforge".to_string(),
152            group: "users".to_string(),
153            size: 0,
154        }
155    }
156}
157
158/// Generation patterns for synthetic files
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub enum GenerationPattern {
161    Random,
162    Zeros,
163    Ones,
164    Incremental,
165}
166
167/// File fixture for configuration
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct FileFixture {
170    pub path: PathBuf,
171    pub content: FileContent,
172    pub metadata: FileMetadata,
173}
174
175impl FileFixture {
176    pub fn to_virtual_file(self) -> VirtualFile {
177        VirtualFile::new(self.path, self.content, self.metadata)
178    }
179}
180
181/// Create a template context with common variables for template rendering
182fn create_template_context() -> Value {
183    let mut context = serde_json::Map::new();
184
185    // Add current timestamp
186    let now = Utc::now();
187    context.insert("now".to_string(), Value::String(now.to_rfc3339()));
188    context.insert("timestamp".to_string(), Value::Number(now.timestamp().into()));
189    context.insert("date".to_string(), Value::String(now.format("%Y-%m-%d").to_string()));
190    context.insert("time".to_string(), Value::String(now.format("%H:%M:%S").to_string()));
191
192    // Add random values
193    context.insert("random_int".to_string(), Value::Number(rand::random::<i64>().into()));
194    context.insert(
195        "random_float".to_string(),
196        Value::String(format!("{:.6}", rand::random::<f64>())),
197    );
198
199    // Add UUID
200    context.insert("uuid".to_string(), Value::String(uuid::Uuid::new_v4().to_string()));
201
202    // Add some sample data
203    let mut faker = serde_json::Map::new();
204    faker.insert("name".to_string(), Value::String("John Doe".to_string()));
205    faker.insert("email".to_string(), Value::String("john.doe@example.com".to_string()));
206    faker.insert("age".to_string(), Value::Number(30.into()));
207    context.insert("faker".to_string(), Value::Object(faker));
208
209    Value::Object(context)
210}