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#[derive(Debug, Subcommand)]
11pub enum FtpCommands {
12 Vfs(VfsCommands),
14 Uploads(UploadsCommands),
16 Fixtures(FixturesCommands),
18}
19
20#[derive(Debug, Args)]
22pub struct VfsCommands {
23 #[command(subcommand)]
24 pub command: VfsSubcommands,
25}
26
27#[derive(Debug, Subcommand)]
28pub enum VfsSubcommands {
29 List,
31 Tree,
33 Add {
35 path: PathBuf,
37 #[arg(short, long)]
39 content: Option<String>,
40 #[arg(short, long)]
42 template: Option<String>,
43 #[arg(short, long)]
45 generate: Option<String>,
46 #[arg(short, long)]
48 size: Option<usize>,
49 },
50 Remove {
52 path: PathBuf,
54 },
55 Clear,
57}
58
59#[derive(Debug, Args)]
61pub struct UploadsCommands {
62 #[command(subcommand)]
63 pub command: UploadsSubcommands,
64}
65
66#[derive(Debug, Subcommand)]
67pub enum UploadsSubcommands {
68 List,
70 Show {
72 id: String,
74 },
75 Export {
77 #[arg(short, long)]
79 dir: PathBuf,
80 },
81}
82
83#[derive(Debug, Args)]
85pub struct FixturesCommands {
86 #[command(subcommand)]
87 pub command: FixturesSubcommands,
88}
89
90#[derive(Debug, Subcommand)]
91pub enum FixturesSubcommands {
92 Load {
94 dir: PathBuf,
96 },
97 List,
99 Reload,
101}
102
103pub 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 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 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 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, ¤t_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 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 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 println!("Fixture reloading not implemented in CLI context.");
392 println!("Fixtures are typically loaded at server startup.");
393 Ok(())
394 }
395 }
396}