mockforge-ftp 0.3.233

FTP protocol support for MockForge
Documentation

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

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

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

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

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

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:

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:

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:

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

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:

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

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 for complete working examples including:

  • Basic FTP server setup
  • Fixture-driven configurations
  • Upload handling scenarios
  • Integration testing patterns

Related Crates

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:

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.