mockforge_ftp/
commands.rs

1use anyhow::Result;
2use clap::{Args, Subcommand};
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use crate::spec_registry::FtpSpecRegistry;
7use crate::vfs::VirtualFileSystem;
8
9/// FTP-related CLI commands
10#[derive(Debug, Subcommand)]
11pub enum FtpCommands {
12    /// Virtual filesystem management
13    Vfs(VfsCommands),
14    /// Upload management
15    Uploads(UploadsCommands),
16    /// Fixture management
17    Fixtures(FixturesCommands),
18}
19
20/// Virtual filesystem commands
21#[derive(Debug, Args)]
22pub struct VfsCommands {
23    #[command(subcommand)]
24    pub command: VfsSubcommands,
25}
26
27#[derive(Debug, Subcommand)]
28pub enum VfsSubcommands {
29    /// List all virtual files
30    List,
31    /// Show directory tree
32    Tree,
33    /// Add a virtual file
34    Add {
35        /// File path
36        path: PathBuf,
37        /// File content
38        #[arg(short, long)]
39        content: Option<String>,
40        /// Template file
41        #[arg(short, long)]
42        template: Option<String>,
43        /// Generate file with pattern
44        #[arg(short, long)]
45        generate: Option<String>,
46        /// File size for generated files
47        #[arg(short, long)]
48        size: Option<usize>,
49    },
50    /// Remove a virtual file
51    Remove {
52        /// File path
53        path: PathBuf,
54    },
55    /// Clear all virtual files
56    Clear,
57}
58
59/// Upload management commands
60#[derive(Debug, Args)]
61pub struct UploadsCommands {
62    #[command(subcommand)]
63    pub command: UploadsSubcommands,
64}
65
66#[derive(Debug, Subcommand)]
67pub enum UploadsSubcommands {
68    /// List received uploads
69    List,
70    /// Show upload details
71    Show {
72        /// Upload ID
73        id: String,
74    },
75    /// Export uploads to directory
76    Export {
77        /// Output directory
78        #[arg(short, long)]
79        dir: PathBuf,
80    },
81}
82
83/// Fixture management commands
84#[derive(Debug, Args)]
85pub struct FixturesCommands {
86    #[command(subcommand)]
87    pub command: FixturesSubcommands,
88}
89
90#[derive(Debug, Subcommand)]
91pub enum FixturesSubcommands {
92    /// Load fixtures from directory
93    Load {
94        /// Directory containing fixture files
95        dir: PathBuf,
96    },
97    /// List loaded fixtures
98    List,
99    /// Reload fixtures
100    Reload,
101}
102
103/// Execute FTP commands
104pub async fn execute_ftp_command(
105    command: FtpCommands,
106    vfs: Arc<VirtualFileSystem>,
107    spec_registry: Arc<FtpSpecRegistry>,
108) -> Result<()> {
109    match command {
110        FtpCommands::Vfs(vfs_cmd) => execute_vfs_command(vfs_cmd, vfs).await,
111        FtpCommands::Uploads(uploads_cmd) => {
112            execute_uploads_command(uploads_cmd, spec_registry).await
113        }
114        FtpCommands::Fixtures(fixtures_cmd) => {
115            execute_fixtures_command(fixtures_cmd, spec_registry).await
116        }
117    }
118}
119
120async fn execute_vfs_command(command: VfsCommands, vfs: Arc<VirtualFileSystem>) -> Result<()> {
121    match command.command {
122        VfsSubcommands::List => {
123            let files = vfs.list_files(&std::path::PathBuf::from("/"));
124            if files.is_empty() {
125                println!("No virtual files found.");
126            } else {
127                println!("Virtual files:");
128                println!("{:<50} {:<10} {:<10} {:<20}", "Path", "Size", "Permissions", "Modified");
129                println!("{}", "-".repeat(90));
130                for file in files {
131                    println!(
132                        "{:<50} {:<10} {:<10} {}",
133                        file.path.display(),
134                        file.metadata.size,
135                        file.metadata.permissions,
136                        file.modified_at.format("%Y-%m-%d %H:%M:%S")
137                    );
138                }
139            }
140            Ok(())
141        }
142        VfsSubcommands::Tree => {
143            let files = vfs.list_files(&std::path::PathBuf::from("/"));
144            if files.is_empty() {
145                println!("No virtual files found.");
146            } else {
147                println!("/");
148                print_tree(&files, &std::path::PathBuf::from("/"), "");
149            }
150            Ok(())
151        }
152        VfsSubcommands::Add {
153            path,
154            content,
155            template,
156            generate,
157            size,
158        } => {
159            use crate::vfs::{FileContent, GenerationPattern, VirtualFile};
160
161            let file_content = if let Some(content) = content {
162                FileContent::Static(content.into_bytes())
163            } else if let Some(template) = template {
164                FileContent::Template(template)
165            } else if let Some(pattern) = generate {
166                let gen_pattern = match pattern.as_str() {
167                    "random" => GenerationPattern::Random,
168                    "zeros" => GenerationPattern::Zeros,
169                    "ones" => GenerationPattern::Ones,
170                    "incremental" => GenerationPattern::Incremental,
171                    _ => {
172                        println!(
173                            "Invalid generation pattern. Use: random, zeros, ones, incremental"
174                        );
175                        return Ok(());
176                    }
177                };
178                let file_size = size.unwrap_or(1024);
179                FileContent::Generated {
180                    size: file_size,
181                    pattern: gen_pattern,
182                }
183            } else {
184                println!("Must specify one of: --content, --template, or --generate");
185                return Ok(());
186            };
187
188            let virtual_file = VirtualFile::new(path.clone(), file_content, Default::default());
189
190            vfs.add_file(path.clone(), virtual_file)?;
191            println!("Added virtual file: {}", path.display());
192            Ok(())
193        }
194        VfsSubcommands::Remove { path } => {
195            vfs.remove_file(&path)?;
196            println!("Removed virtual file: {}", path.display());
197            Ok(())
198        }
199        VfsSubcommands::Clear => {
200            vfs.clear()?;
201            println!("Cleared all virtual files.");
202            Ok(())
203        }
204    }
205}
206
207fn print_tree(files: &[crate::vfs::VirtualFile], current_path: &std::path::Path, prefix: &str) {
208    use std::collections::HashMap;
209
210    let mut dirs: HashMap<String, Vec<crate::vfs::VirtualFile>> = HashMap::new();
211    let mut current_files = Vec::new();
212
213    for file in files {
214        if let Ok(relative) = file.path.strip_prefix(current_path) {
215            let components: Vec<_> = relative.components().collect();
216            if components.len() == 1 {
217                // File in current directory
218                current_files.push(file.clone());
219            } else if let std::path::Component::Normal(name) = components[0] {
220                let dir_name = name.to_string_lossy().to_string();
221                let sub_path = current_path.join(&dir_name);
222                let remaining_path = components[1..].iter().collect::<std::path::PathBuf>();
223                let full_sub_path = sub_path.join(remaining_path);
224
225                dirs.entry(dir_name).or_default().push(crate::vfs::VirtualFile {
226                    path: full_sub_path,
227                    ..file.clone()
228                });
229            }
230        }
231    }
232
233    // Print files in current directory
234    for (i, file) in current_files.iter().enumerate() {
235        let is_last = i == current_files.len() - 1 && dirs.is_empty();
236        let connector = if is_last { "└── " } else { "├── " };
237        println!(
238            "{}{}{}",
239            prefix,
240            connector,
241            file.path.file_name().unwrap_or_default().to_string_lossy()
242        );
243    }
244
245    // Print subdirectories
246    let dir_keys: Vec<_> = dirs.keys().cloned().collect();
247    for (i, dir_name) in dir_keys.iter().enumerate() {
248        let is_last = i == dir_keys.len() - 1;
249        let connector = if is_last { "└── " } else { "├── " };
250        let new_prefix = format!("{}{}", prefix, if is_last { "    " } else { "│   " });
251
252        println!("{}{}{}/", prefix, connector, dir_name);
253
254        if let Some(sub_files) = dirs.get(dir_name) {
255            print_tree(sub_files, &current_path.join(dir_name), &new_prefix);
256        }
257    }
258}
259
260async fn execute_uploads_command(
261    command: UploadsCommands,
262    spec_registry: Arc<FtpSpecRegistry>,
263) -> Result<()> {
264    match command.command {
265        UploadsSubcommands::List => {
266            let uploads = spec_registry.get_uploads();
267            if uploads.is_empty() {
268                println!("No uploads found.");
269            } else {
270                println!("Uploaded files:");
271                println!("{:<40} {:<50} {:<10} {:<20}", "ID", "Path", "Size", "Uploaded");
272                println!("{}", "-".repeat(120));
273                for upload in uploads {
274                    println!(
275                        "{:<40} {:<50} {:<10} {}",
276                        upload.id,
277                        upload.path.display(),
278                        upload.size,
279                        upload.uploaded_at.format("%Y-%m-%d %H:%M:%S")
280                    );
281                }
282            }
283            Ok(())
284        }
285        UploadsSubcommands::Show { id } => {
286            if let Some(upload) = spec_registry.get_upload(&id) {
287                println!("Upload Details:");
288                println!("ID: {}", upload.id);
289                println!("Path: {}", upload.path.display());
290                println!("Size: {} bytes", upload.size);
291                println!("Uploaded: {}", upload.uploaded_at.format("%Y-%m-%d %H:%M:%S"));
292                if let Some(rule) = &upload.rule_name {
293                    println!("Rule: {}", rule);
294                }
295            } else {
296                println!("Upload with ID '{}' not found.", id);
297            }
298            Ok(())
299        }
300        UploadsSubcommands::Export { dir } => {
301            use tokio::fs;
302
303            // Create directory if it doesn't exist
304            fs::create_dir_all(&dir).await?;
305
306            let uploads = spec_registry.get_uploads();
307            if uploads.is_empty() {
308                println!("No uploads to export.");
309                return Ok(());
310            }
311
312            for upload in uploads {
313                if let Some(file) = spec_registry.vfs.get_file(&upload.path) {
314                    if let Ok(content) = file.render_content() {
315                        let export_path =
316                            dir.join(upload.path.strip_prefix("/").unwrap_or(&upload.path));
317                        if let Some(parent) = export_path.parent() {
318                            fs::create_dir_all(parent).await?;
319                        }
320                        fs::write(&export_path, content).await?;
321                        println!(
322                            "Exported: {} -> {}",
323                            upload.path.display(),
324                            export_path.display()
325                        );
326                    }
327                }
328            }
329            println!("Export complete.");
330            Ok(())
331        }
332    }
333}
334
335async fn execute_fixtures_command(
336    command: FixturesCommands,
337    spec_registry: Arc<FtpSpecRegistry>,
338) -> Result<()> {
339    match command.command {
340        FixturesSubcommands::Load { dir } => {
341            use serde_yaml;
342            use std::fs;
343
344            let mut loaded_fixtures = Vec::new();
345
346            for entry in fs::read_dir(&dir)? {
347                let entry = entry?;
348                let path = entry.path();
349
350                if path.extension().and_then(|s| s.to_str()) == Some("yaml")
351                    || path.extension().and_then(|s| s.to_str()) == Some("yml")
352                {
353                    let content = fs::read_to_string(&path)?;
354                    let fixture: crate::fixtures::FtpFixture = serde_yaml::from_str(&content)?;
355                    loaded_fixtures.push(fixture);
356                    println!("Loaded fixture: {}", path.display());
357                }
358            }
359
360            if !loaded_fixtures.is_empty() {
361                // Note: This is a simplified implementation. In a real scenario,
362                // we'd need to update the spec_registry properly, but since it's Arc,
363                // we'd need a different approach. For now, we'll just report what we found.
364                println!("Found {} fixture files. (Note: Loading not fully implemented in this CLI context)", loaded_fixtures.len());
365            } else {
366                println!("No YAML fixture files found in {}", dir.display());
367            }
368
369            Ok(())
370        }
371        FixturesSubcommands::List => {
372            if spec_registry.fixtures.is_empty() {
373                println!("No fixtures loaded.");
374            } else {
375                println!("Loaded fixtures:");
376                for fixture in &spec_registry.fixtures {
377                    println!("- {}: {}", fixture.identifier, fixture.name);
378                    if let Some(desc) = &fixture.description {
379                        println!("  Description: {}", desc);
380                    }
381                    println!("  Virtual files: {}", fixture.virtual_files.len());
382                    println!("  Upload rules: {}", fixture.upload_rules.len());
383                    println!();
384                }
385            }
386            Ok(())
387        }
388        FixturesSubcommands::Reload => {
389            // Clear existing fixtures and reload from configured directories
390            // This is a simplified implementation
391            println!("Fixture reloading not implemented in CLI context.");
392            println!("Fixtures are typically loaded at server startup.");
393            Ok(())
394        }
395    }
396}