use anyhow::Result;
use clap::{Args, Subcommand};
use std::path::{Component, PathBuf};
use std::sync::Arc;
use crate::spec_registry::FtpSpecRegistry;
use crate::vfs::VirtualFileSystem;
#[derive(Debug, Subcommand)]
pub enum FtpCommands {
Vfs(VfsCommands),
Uploads(UploadsCommands),
Fixtures(FixturesCommands),
}
#[derive(Debug, Args)]
pub struct VfsCommands {
#[command(subcommand)]
pub command: VfsSubcommands,
}
#[derive(Debug, Subcommand)]
pub enum VfsSubcommands {
List,
Tree,
Add {
path: PathBuf,
#[arg(short, long)]
content: Option<String>,
#[arg(short, long)]
template: Option<String>,
#[arg(short, long)]
generate: Option<String>,
#[arg(short, long)]
size: Option<usize>,
},
Remove {
path: PathBuf,
},
Clear,
}
#[derive(Debug, Args)]
pub struct UploadsCommands {
#[command(subcommand)]
pub command: UploadsSubcommands,
}
#[derive(Debug, Subcommand)]
pub enum UploadsSubcommands {
List,
Show {
id: String,
},
Export {
#[arg(short, long)]
dir: PathBuf,
},
}
#[derive(Debug, Args)]
pub struct FixturesCommands {
#[command(subcommand)]
pub command: FixturesSubcommands,
}
#[derive(Debug, Subcommand)]
pub enum FixturesSubcommands {
Load {
dir: PathBuf,
},
List,
Reload,
}
pub async fn execute_ftp_command(
command: FtpCommands,
vfs: Arc<VirtualFileSystem>,
spec_registry: Arc<FtpSpecRegistry>,
) -> Result<()> {
match command {
FtpCommands::Vfs(vfs_cmd) => execute_vfs_command(vfs_cmd, vfs).await,
FtpCommands::Uploads(uploads_cmd) => {
execute_uploads_command(uploads_cmd, spec_registry).await
}
FtpCommands::Fixtures(fixtures_cmd) => {
execute_fixtures_command(fixtures_cmd, spec_registry).await
}
}
}
async fn execute_vfs_command(command: VfsCommands, vfs: Arc<VirtualFileSystem>) -> Result<()> {
match command.command {
VfsSubcommands::List => {
let files = vfs.list_files(&PathBuf::from("/"));
if files.is_empty() {
println!("No virtual files found.");
} else {
println!("Virtual files:");
println!("{:<50} {:<10} {:<10} {:<20}", "Path", "Size", "Permissions", "Modified");
println!("{}", "-".repeat(90));
for file in files {
println!(
"{:<50} {:<10} {:<10} {}",
file.path.display(),
file.metadata.size,
file.metadata.permissions,
file.modified_at.format("%Y-%m-%d %H:%M:%S")
);
}
}
Ok(())
}
VfsSubcommands::Tree => {
let files = vfs.list_files(&PathBuf::from("/"));
if files.is_empty() {
println!("No virtual files found.");
} else {
println!("/");
print_tree(&files, &PathBuf::from("/"), "");
}
Ok(())
}
VfsSubcommands::Add {
path,
content,
template,
generate,
size,
} => {
use crate::vfs::{FileContent, GenerationPattern, VirtualFile};
let file_content = if let Some(content) = content {
FileContent::Static(content.into_bytes())
} else if let Some(template) = template {
FileContent::Template(template)
} else if let Some(pattern) = generate {
let gen_pattern = match pattern.as_str() {
"random" => GenerationPattern::Random,
"zeros" => GenerationPattern::Zeros,
"ones" => GenerationPattern::Ones,
"incremental" => GenerationPattern::Incremental,
_ => {
println!(
"Invalid generation pattern. Use: random, zeros, ones, incremental"
);
return Ok(());
}
};
let file_size = size.unwrap_or(1024);
FileContent::Generated {
size: file_size,
pattern: gen_pattern,
}
} else {
println!("Must specify one of: --content, --template, or --generate");
return Ok(());
};
let virtual_file = VirtualFile::new(path.clone(), file_content, Default::default());
vfs.add_file(path.clone(), virtual_file)?;
println!("Added virtual file: {}", path.display());
Ok(())
}
VfsSubcommands::Remove { path } => {
vfs.remove_file(&path)?;
println!("Removed virtual file: {}", path.display());
Ok(())
}
VfsSubcommands::Clear => {
vfs.clear()?;
println!("Cleared all virtual files.");
Ok(())
}
}
}
fn print_tree(files: &[crate::vfs::VirtualFile], current_path: &std::path::Path, prefix: &str) {
use std::collections::HashMap;
let mut dirs: HashMap<String, Vec<crate::vfs::VirtualFile>> = HashMap::new();
let mut current_files = Vec::new();
for file in files {
if let Ok(relative) = file.path.strip_prefix(current_path) {
let components: Vec<_> = relative.components().collect();
if components.len() == 1 {
current_files.push(file.clone());
} else if let Component::Normal(name) = components[0] {
let dir_name = name.to_string_lossy().to_string();
let sub_path = current_path.join(&dir_name);
let remaining_path = components[1..].iter().collect::<PathBuf>();
let full_sub_path = sub_path.join(remaining_path);
dirs.entry(dir_name).or_default().push(crate::vfs::VirtualFile {
path: full_sub_path,
..file.clone()
});
}
}
}
for (i, file) in current_files.iter().enumerate() {
let is_last = i == current_files.len() - 1 && dirs.is_empty();
let connector = if is_last { "└── " } else { "├── " };
println!(
"{}{}{}",
prefix,
connector,
file.path.file_name().unwrap_or_default().to_string_lossy()
);
}
let dir_keys: Vec<_> = dirs.keys().cloned().collect();
for (i, dir_name) in dir_keys.iter().enumerate() {
let is_last = i == dir_keys.len() - 1;
let connector = if is_last { "└── " } else { "├── " };
let new_prefix = format!("{}{}", prefix, if is_last { " " } else { "│ " });
println!("{}{}{}/", prefix, connector, dir_name);
if let Some(sub_files) = dirs.get(dir_name) {
print_tree(sub_files, ¤t_path.join(dir_name), &new_prefix);
}
}
}
async fn execute_uploads_command(
command: UploadsCommands,
spec_registry: Arc<FtpSpecRegistry>,
) -> Result<()> {
match command.command {
UploadsSubcommands::List => {
let uploads = spec_registry.get_uploads();
if uploads.is_empty() {
println!("No uploads found.");
} else {
println!("Uploaded files:");
println!("{:<40} {:<50} {:<10} {:<20}", "ID", "Path", "Size", "Uploaded");
println!("{}", "-".repeat(120));
for upload in uploads {
println!(
"{:<40} {:<50} {:<10} {}",
upload.id,
upload.path.display(),
upload.size,
upload.uploaded_at.format("%Y-%m-%d %H:%M:%S")
);
}
}
Ok(())
}
UploadsSubcommands::Show { id } => {
if let Some(upload) = spec_registry.get_upload(&id) {
println!("Upload Details:");
println!("ID: {}", upload.id);
println!("Path: {}", upload.path.display());
println!("Size: {} bytes", upload.size);
println!("Uploaded: {}", upload.uploaded_at.format("%Y-%m-%d %H:%M:%S"));
if let Some(rule) = &upload.rule_name {
println!("Rule: {}", rule);
}
} else {
println!("Upload with ID '{}' not found.", id);
}
Ok(())
}
UploadsSubcommands::Export { dir } => {
use tokio::fs;
fs::create_dir_all(&dir).await?;
let uploads = spec_registry.get_uploads();
if uploads.is_empty() {
println!("No uploads to export.");
return Ok(());
}
for upload in uploads {
if let Some(file) = spec_registry.vfs.get_file(&upload.path) {
if let Ok(content) = file.render_content() {
let export_path =
dir.join(upload.path.strip_prefix("/").unwrap_or(&upload.path));
if let Some(parent) = export_path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(&export_path, content).await?;
println!(
"Exported: {} -> {}",
upload.path.display(),
export_path.display()
);
}
}
}
println!("Export complete.");
Ok(())
}
}
}
async fn execute_fixtures_command(
command: FixturesCommands,
spec_registry: Arc<FtpSpecRegistry>,
) -> Result<()> {
match command.command {
FixturesSubcommands::Load { dir } => {
use mockforge_core::fixture_store::{
load_fixtures_from_dir, FixtureFileFormat, FixtureFileGranularity,
FixtureLoadErrorMode, FixtureLoadOptions,
};
let options = FixtureLoadOptions {
formats: vec![FixtureFileFormat::Yaml],
error_mode: FixtureLoadErrorMode::FailFast,
granularity: FixtureFileGranularity::Single,
};
let loaded_fixtures: Vec<crate::fixtures::FtpFixture> =
load_fixtures_from_dir(&dir, &options)?;
if !loaded_fixtures.is_empty() {
let mut total_files = 0;
for fixture in &loaded_fixtures {
for virtual_file in &fixture.virtual_files {
let vfs_file = virtual_file.clone().to_file_fixture();
if let Err(e) = spec_registry.vfs.load_fixtures(vec![vfs_file]) {
eprintln!(
"Warning: Failed to load file {}: {}",
virtual_file.path.display(),
e
);
} else {
total_files += 1;
}
}
}
println!(
"Loaded {} fixtures ({} virtual files into VFS)",
loaded_fixtures.len(),
total_files
);
} else {
println!("No YAML fixture files found in {}", dir.display());
}
Ok(())
}
FixturesSubcommands::List => {
if spec_registry.fixtures.is_empty() {
println!("No fixtures loaded.");
} else {
println!("Loaded fixtures:");
for fixture in &spec_registry.fixtures {
println!("- {}: {}", fixture.identifier, fixture.name);
if let Some(desc) = &fixture.description {
println!(" Description: {}", desc);
}
println!(" Virtual files: {}", fixture.virtual_files.len());
println!(" Upload rules: {}", fixture.upload_rules.len());
println!();
}
}
Ok(())
}
FixturesSubcommands::Reload => {
if spec_registry.fixtures.is_empty() {
println!(
"No fixtures loaded. Use `mockforge ftp fixtures load --dir <path>` first."
);
return Ok(());
}
let mut vfs_fixtures = Vec::new();
for fixture in &spec_registry.fixtures {
for virtual_file in &fixture.virtual_files {
vfs_fixtures.push(virtual_file.clone().to_file_fixture());
}
}
spec_registry
.vfs
.load_fixtures(vfs_fixtures)
.map_err(|e| anyhow::anyhow!("Failed to reload fixtures into VFS: {}", e))?;
println!(
"Reloaded {} fixture(s) into virtual filesystem.",
spec_registry.fixtures.len()
);
Ok(())
}
}
}