# MockForge FTP
FTP protocol support for MockForge.
This crate provides FTP-specific functionality for creating mock FTP servers, including virtual file systems, fixture-driven responses, and file transfer simulation. Perfect for testing FTP clients, file upload workflows, and FTP-based integrations.
## Features
- **FTP Server Mocking**: Full FTP protocol support with standard commands
- **Virtual File System**: In-memory file storage with configurable content
- **Fixture Management**: YAML-based configuration for file structures
- **Upload Handling**: Configurable validation and storage for file uploads
- **Template Support**: Dynamic content generation using Handlebars templates
- **Protocol Compliance**: Supports passive/active modes and authentication
## Quick Start
### Basic FTP Server
```rust,no_run
use mockforge_ftp::FtpServer;
use mockforge_core::config::FtpConfig;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create default FTP configuration
let config = FtpConfig::default();
// Initialize FTP server
let server = FtpServer::new(config);
// Start the server
server.start().await?;
Ok(())
}
```
### Server with Custom Configuration
```rust,no_run
use mockforge_core::config::FtpConfig;
use mockforge_ftp::FtpServer;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = FtpConfig {
host: "127.0.0.1".to_string(),
port: 2121,
virtual_root: "/ftp".into(),
..Default::default()
};
let server = FtpServer::new(config);
server.start().await?;
Ok(())
}
```
## Virtual File System
MockForge FTP includes a powerful virtual file system for creating realistic file structures:
### Static Files
```rust,no_run
use mockforge_ftp::vfs::{VirtualFileSystem, VirtualFile, FileContent, FileMetadata};
use std::path::PathBuf;
fn main() -> anyhow::Result<()> {
let vfs = VirtualFileSystem::new(PathBuf::from("/ftp"));
// Add a static file
let content = FileContent::Static(b"Hello, FTP!".to_vec());
let metadata = FileMetadata {
size: 11,
permissions: "644".to_string(),
owner: "user".to_string(),
group: "users".to_string(),
};
let file = VirtualFile::new(
PathBuf::from("/ftp/hello.txt"),
content,
metadata
);
vfs.add_file(PathBuf::from("/ftp/hello.txt"), file)?;
Ok(())
}
```
### Generated Files
```rust,no_run
use mockforge_ftp::vfs::{FileContent, GenerationPattern};
// Generate a 1MB file with random content
let content = FileContent::Generated {
size: 1024 * 1024,
pattern: GenerationPattern::Random,
};
// Generate a file with incremental bytes (0, 1, 2, ...)
let incremental = FileContent::Generated {
size: 1000,
pattern: GenerationPattern::Incremental,
};
```
### Template Files
```rust,no_run
use mockforge_ftp::vfs::FileContent;
// Use Handlebars templates for dynamic content
let content = FileContent::Template(
"Hello {{name}}! Today is {{date}}.".to_string()
);
```
## Fixture System
Define complete FTP server configurations using YAML fixtures:
```yaml
identifier: "test-server"
name: "Test FTP Server"
description: "A test FTP server with sample files"
virtual_files:
- path: "/welcome.txt"
content:
type: "static"
content: "Welcome to MockForge FTP!"
permissions: "644"
owner: "ftp"
group: "ftp"
- path: "/data.bin"
content:
type: "generated"
size: 1048576 # 1MB
pattern: "Random"
permissions: "644"
owner: "ftp"
group: "ftp"
upload_rules:
- path_pattern: "/uploads/.*\\.txt"
auto_accept: true
validation:
max_size_bytes: 1048576 # 1MB limit
allowed_extensions: ["txt"]
storage:
type: "memory"
- path_pattern: "/logs/.*\\.log"
auto_accept: true
storage:
type: "file"
path: "/tmp/ftp_logs"
```
### Loading Fixtures
Load a YAML file before constructing the server:
```rust,no_run
use mockforge_core::config::FtpConfig;
use mockforge_ftp::{FtpFixture, FtpServer};
fn main() -> anyhow::Result<()> {
let fixture = FtpFixture::from_yaml_file("ftp-fixture.yaml")?;
let server = FtpServer::new_with_fixtures(FtpConfig::default(), vec![fixture])?;
assert_eq!(server.spec_registry().fixtures.len(), 1);
Ok(())
}
```
For programmatic fixtures in an async application:
```rust,no_run
use mockforge_ftp::{FtpServer, FtpFixture, VirtualFileConfig};
use mockforge_ftp::fixtures::FileContentConfig;
use mockforge_core::config::FtpConfig;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Fixtures are attached at construction time via the registry builder.
let fixture = FtpFixture {
identifier: "programmatic".to_string(),
name: "Programmatic Fixture".to_string(),
description: Some("Created in code".to_string()),
virtual_files: vec![VirtualFileConfig {
path: "/pub/hello.txt".into(),
content: FileContentConfig::Static {
content: "hello world".to_string(),
},
permissions: "644".to_string(),
owner: "mockforge".to_string(),
group: "mockforge".to_string(),
}],
upload_rules: vec![],
};
let config = FtpConfig::default();
let server = FtpServer::new_with_fixtures(config, vec![fixture])?;
// Virtual files from the fixture are served by the server's VFS:
let file = server.vfs().get_file_async(std::path::Path::new("/pub/hello.txt")).await;
assert!(file.is_some());
Ok(())
}
```
`FtpServer::new(config)` starts with an empty registry — `FtpSpecRegistry`
fixtures are bound at construction and cannot be mutated afterwards
(`spec_registry()` hands out a shared immutable handle). `FtpServer::new` is a
synchronous constructor; only `start()` is async. The registry's
`with_fixtures` builder consumes and returns a registry; it cannot be called on
the immutable handle returned by an already-created server. Use the server
constructor above instead. Use the VFS's async accessors inside async tasks.
## Upload Handling
Configure how the server handles file uploads:
### Upload Rules
```rust,no_run
use mockforge_ftp::fixtures::{UploadRule, FileValidation, UploadStorage};
use regex::Regex;
let rule = UploadRule {
path_pattern: r"/uploads/.*".to_string(),
auto_accept: true,
validation: Some(FileValidation {
max_size_bytes: Some(10 * 1024 * 1024), // 10MB
allowed_extensions: Some(vec!["txt".to_string(), "csv".to_string()]),
mime_types: Some(vec!["text/plain".to_string(), "text/csv".to_string()]),
}),
storage: UploadStorage::Memory, // Store in VFS
};
```
### Storage Options
- **Memory**: Store uploaded files in the virtual file system
- **File**: Write to the local file system
- **Discard**: Accept uploads but don't store them
## FTP Protocol Support
MockForge FTP supports standard FTP commands:
- **Connection**: USER, PASS, QUIT
- **Navigation**: CWD, CDUP, PWD
- **Directory**: LIST, NLST, MKD, RMD
- **File Operations**: RETR, STOR, DELE, RNFR/RNTO
- **System**: SYST, FEAT, OPTS
- **Transfer Modes**: Passive and active mode support
## Testing FTP Clients
Use MockForge FTP to test FTP client applications:
```rust,no_run
use suppaftp::FtpStream; // FTP client library
use mockforge_ftp::{FtpServer, VirtualFile, FileContent, FileMetadata};
use mockforge_core::config::FtpConfig;
use tokio::task;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Start FTP server in background
let config = FtpConfig {
host: "127.0.0.1".to_string(),
port: 2121,
virtual_root: "/".into(),
..Default::default()
};
let server = FtpServer::new(config.clone());
let welcome = VirtualFile::new(
"/welcome.txt".into(),
FileContent::Static(b"Welcome to MockForge FTP!".to_vec()),
FileMetadata::default(),
);
server.vfs().add_file_async("/welcome.txt".into(), welcome).await?;
task::spawn(async move {
server.start().await.unwrap();
});
// Give server time to start
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Test FTP client
let mut ftp_stream = FtpStream::connect(format!("{}:{}", config.host, config.port))?;
ftp_stream.login("anonymous", "anonymous")?;
// List files
let files = ftp_stream.list(None)?;
println!("Files: {:?}", files);
// Download a file
let mut reader = ftp_stream.retr_as_buffer("welcome.txt")?;
// ... process file content ...
ftp_stream.quit()?;
Ok(())
}
```
## Configuration
### FtpConfig
```rust,no_run
use mockforge_core::config::FtpConfig;
let config = FtpConfig {
host: "0.0.0.0".to_string(), // Bind address
port: 21, // FTP port
virtual_root: "/".into(), // Virtual filesystem root
max_connections: 100, // Connection limit
..Default::default()
};
```
## Integration with MockForge
MockForge FTP integrates seamlessly with the MockForge ecosystem:
- **MockForge Core**: Shared configuration and logging
- **MockForge CLI**: Command-line interface for FTP server management
- **MockForge Plugins**: Extend FTP functionality with custom plugins
## Performance
MockForge FTP is optimized for testing scenarios:
- **In-Memory Storage**: Fast file operations without disk I/O
- **Lazy Content Generation**: Files generated on-demand
- **Concurrent Connections**: Handle multiple simultaneous FTP clients
- **Low Memory Footprint**: Efficient storage for large file simulations
## Examples
See the [examples directory](https://github.com/SaaSy-Solutions/mockforge/tree/main/examples) for complete working examples including:
- Basic FTP server setup
- Fixture-driven configurations
- Upload handling scenarios
- Integration testing patterns
## Related Crates
- [`mockforge-core`](https://docs.rs/mockforge-core): Core mocking functionality
- [`libunftp`](https://docs.rs/libunftp): Underlying FTP server implementation
- [`suppaftp`](https://docs.rs/suppaftp): FTP client for testing
## License
Licensed under MIT OR Apache-2.0
## Starting the CLI with configuration
For a CLI built with the `ftp` feature, `mockforge ftp serve --config ftp-config.yaml`
loads either standalone FTP settings or the `ftp` section of a full server YAML.
For example:
```yaml
host: 127.0.0.1
port: 2121
virtual_root: /
fixtures_dir: fixtures
passive_ports: [50000, 50100]
timeout_secs: 300
```
`fixtures_dir` is resolved relative to the configuration file, independently of
the working directory. Each `.yaml` or `.yml` file contains one `FtpFixture`
using the fixture schema above. Missing or invalid configuration and fixture
files fail startup with a diagnostic.
Explicit `--port`, `--host`, and `--virtual-root` flags override file values.
Without a configuration file, the standalone CLI retains port 2121, host
127.0.0.1, and virtual root `/`. File settings use `FtpConfig` defaults for omitted
fields. The passive port range and idle timeout apply to actual FTP connections.
Authentication policy and connection caps remain incomplete: the listener uses
libunftp's anonymous authenticator and does not currently enforce
`allow_anonymous: false` or `max_connections`. These fields do not provide access
control or a concurrency limit.