use anyhow::Result;
use clap::Subcommand;
use mockforge_core::config::FtpConfig;
use mockforge_ftp::{
FileContent, FileMetadata, FtpServer, GenerationPattern, VirtualFile, VirtualFileSystem,
};
use std::path::PathBuf;
#[derive(Subcommand)]
pub enum FtpCommands {
#[command(verbatim_doc_comment)]
Serve {
#[arg(short, long, default_value = "2121")]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(short, long)]
config: Option<PathBuf>,
#[arg(long, default_value = "/")]
virtual_root: String,
},
#[command(verbatim_doc_comment)]
Fixtures {
#[command(subcommand)]
fixtures_command: FtpFixturesCommands,
},
#[command(verbatim_doc_comment)]
Vfs {
#[command(subcommand)]
vfs_command: FtpVfsCommands,
},
}
#[derive(Subcommand)]
pub enum FtpFixturesCommands {
List,
Load {
directory: PathBuf,
},
Validate {
file: PathBuf,
},
}
#[derive(Subcommand)]
pub enum FtpVfsCommands {
List {
path: String,
},
#[command(verbatim_doc_comment)]
Add {
path: String,
#[arg(long, conflicts_with_all = ["template", "generate"])]
content: Option<String>,
#[arg(long, conflicts_with_all = ["content", "generate"])]
template: Option<String>,
#[arg(long, conflicts_with_all = ["content", "template"], value_enum)]
generate: Option<GenerationType>,
#[arg(long, requires = "generate")]
size: Option<usize>,
},
Remove {
path: String,
},
Info {
path: String,
},
}
#[derive(clap::ValueEnum, Clone)]
pub enum GenerationType {
Random,
Zeros,
Ones,
Incremental,
}
pub async fn handle_ftp_command(command: FtpCommands) -> Result<()> {
match command {
FtpCommands::Serve {
port,
host,
config,
virtual_root,
} => handle_ftp_serve(port, host, config, virtual_root).await,
FtpCommands::Fixtures { fixtures_command } => handle_ftp_fixtures(fixtures_command).await,
FtpCommands::Vfs { vfs_command } => handle_ftp_vfs(vfs_command).await,
}
}
async fn handle_ftp_serve(
port: u16,
host: String,
_config: Option<PathBuf>,
virtual_root: String,
) -> Result<()> {
println!("Starting FTP server on {}:{}", host, port);
let config = FtpConfig {
host,
port,
virtual_root: virtual_root.into(),
..Default::default()
};
let server = FtpServer::new(config);
server.start().await?;
Ok(())
}
async fn handle_ftp_fixtures(command: FtpFixturesCommands) -> Result<()> {
match command {
FtpFixturesCommands::List => {
let cwd = std::env::current_dir()?;
let fixture_dirs = ["fixtures/ftp", "fixtures", "ftp-fixtures"];
let mut found_any = false;
println!("FTP fixtures:");
for dir_name in &fixture_dirs {
let dir = cwd.join(dir_name);
if !dir.exists() {
continue;
}
for entry in std::fs::read_dir(&dir)?.flatten() {
let path = entry.path();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if matches!(ext, "yaml" | "yml") {
if let Ok(content) = std::fs::read_to_string(&path) {
match serde_yaml::from_str::<mockforge_ftp::FtpFixture>(&content) {
Ok(fixture) => {
println!(
" {} - {} ({} files, {} upload rules)",
fixture.identifier,
fixture.name,
fixture.virtual_files.len(),
fixture.upload_rules.len()
);
found_any = true;
}
Err(_) => {
}
}
}
}
}
}
if !found_any {
println!(" No fixtures found. Place YAML fixture files in fixtures/ftp/");
}
}
FtpFixturesCommands::Load { directory } => {
println!("Loading FTP fixtures from: {}", directory.display());
if !directory.exists() {
anyhow::bail!("Directory does not exist: {}", directory.display());
}
let mut loaded = 0;
let mut errors = 0;
for entry in std::fs::read_dir(&directory)?.flatten() {
let path = entry.path();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !matches!(ext, "yaml" | "yml") {
continue;
}
let content = std::fs::read_to_string(&path)?;
match serde_yaml::from_str::<mockforge_ftp::FtpFixture>(&content) {
Ok(fixture) => {
println!(
" Loaded: {} ({} files, {} upload rules)",
fixture.name,
fixture.virtual_files.len(),
fixture.upload_rules.len()
);
loaded += 1;
}
Err(e) => {
println!(" Error in {}: {}", path.display(), e);
errors += 1;
}
}
}
println!("Loaded {} fixture(s), {} error(s)", loaded, errors);
}
FtpFixturesCommands::Validate { file } => {
println!("Validating FTP fixture: {}", file.display());
if !file.exists() {
anyhow::bail!("File does not exist: {}", file.display());
}
let content = std::fs::read_to_string(&file)?;
match serde_yaml::from_str::<mockforge_ftp::FtpFixture>(&content) {
Ok(fixture) => {
println!(" Valid fixture: {}", fixture.name);
println!(" Identifier: {}", fixture.identifier);
println!(
" Description: {}",
fixture.description.as_deref().unwrap_or("(none)")
);
println!(" Virtual files: {}", fixture.virtual_files.len());
for vf in &fixture.virtual_files {
println!(" - {} ({})", vf.path.display(), vf.permissions);
}
println!(" Upload rules: {}", fixture.upload_rules.len());
for rule in &fixture.upload_rules {
println!(
" - pattern: {} (auto_accept: {})",
rule.path_pattern, rule.auto_accept
);
}
}
Err(e) => {
println!(" Invalid fixture: {}", e);
std::process::exit(1);
}
}
}
}
Ok(())
}
async fn handle_ftp_vfs(command: FtpVfsCommands) -> Result<()> {
let vfs = VirtualFileSystem::new(PathBuf::from("/"));
match command {
FtpVfsCommands::List { path } => {
println!("Files in {}:", path);
let files = vfs.list_files(PathBuf::from(path).as_path());
for file in files {
println!(" {} ({} bytes)", file.path.display(), file.metadata.size);
}
}
FtpVfsCommands::Add {
path,
content,
template,
generate,
size,
} => {
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(gen_type) = generate {
let size = size.unwrap_or(1024);
let pattern = match gen_type {
GenerationType::Random => GenerationPattern::Random,
GenerationType::Zeros => GenerationPattern::Zeros,
GenerationType::Ones => GenerationPattern::Ones,
GenerationType::Incremental => GenerationPattern::Incremental,
};
FileContent::Generated { size, pattern }
} else {
FileContent::Static(b"".to_vec())
};
let file = VirtualFile::new(
PathBuf::from(path.clone()),
file_content,
FileMetadata::default(),
);
vfs.add_file(PathBuf::from(path), file)?;
println!("File added to virtual file system");
}
FtpVfsCommands::Remove { path } => {
if vfs.remove_file(PathBuf::from(path).as_path()).is_ok() {
println!("File removed from virtual file system");
} else {
println!("File not found");
}
}
FtpVfsCommands::Info { path } => {
if let Some(file) = vfs.get_file(PathBuf::from(path).as_path()) {
println!("File: {}", file.path.display());
println!("Size: {} bytes", file.metadata.size);
println!("Permissions: {}", file.metadata.permissions);
println!("Owner: {}", file.metadata.owner);
println!("Modified: {}", file.modified_at);
} else {
println!("File not found");
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ftp_commands_serve_variant() {
let _cmd = FtpCommands::Serve {
port: 2121,
host: "127.0.0.1".to_string(),
config: None,
virtual_root: "/".to_string(),
};
}
#[test]
fn test_ftp_fixtures_list_variant() {
let _cmd = FtpFixturesCommands::List;
}
#[test]
fn test_ftp_fixtures_load_variant() {
let _cmd = FtpFixturesCommands::Load {
directory: PathBuf::from("./fixtures/ftp"),
};
}
#[test]
fn test_ftp_fixtures_validate_variant() {
let _cmd = FtpFixturesCommands::Validate {
file: PathBuf::from("fixture.yaml"),
};
}
#[test]
fn test_ftp_vfs_list_variant() {
let _cmd = FtpVfsCommands::List {
path: "/".to_string(),
};
}
}