Skip to content

feat: Add FTP provider for plain FTP server browsing #236

@BrianLeishman

Description

@BrianLeishman

Summary

Add a plain FTP (ftp://) provider alongside the existing SFTP provider. FTP and SFTP are completely different protocols (FTP = plain TCP on port 21, SFTP = SSH on port 22), but from the user's perspective they serve the same purpose: browse, upload, and download files on remote servers. The SFTP implementation provides all the patterns to follow.

Current State

SFTP provider is fully implemented across these files:

  • Rust provider: src-tauri/src/locations/sftp/mod.rs (SftpProvider, LocationProvider trait, URL parsing, cross-provider helpers)
  • Auth/credentials: src-tauri/src/locations/sftp/auth.rs (keychain storage, server list in ~/.config/marlin/sftp-servers.json)
  • Connection pool: src-tauri/src/locations/sftp/pool.rs (session caching, 15s connect timeout, 5min idle eviction)
  • Commands: src-tauri/src/commands.rs:7059-7190 (get/add/remove servers, test connection, download, window lifecycle)
  • Cross-provider paste: src-tauri/src/commands.rs:2519-2716 (sftp↔file, sftp↔gdrive, sftp↔smb)
  • Thumbnails: src-tauri/src/thumbnails/generators/sftp.rs (download to temp, generate locally)
  • Frontend types: src/types/index.ts:283-303 (SftpServerInfo, SftpConnectInitPayload, SftpConnectSuccessPayload)
  • Store: src/store/useAppStore.ts (sftpServers state, pendingSftpCredentialRequest, credential error detection)
  • Connect window: src/windows/SftpConnectWindow.tsx (form with hostname/port/username/auth method)
  • Sidebar: src/components/Sidebar.tsx:745-809 (server list in Network section, disconnect, "Add SFTP Server..." button)
  • Events: src/utils/events.ts:3-4 (sftp-connect:init, sftp-connect:success)

No FTP support exists currently.

Proposed Changes

Rust Backend

  1. New module: src-tauri/src/locations/ftp/ with mod.rs, auth.rs, pool.rs

    • Use suppaftp crate with tokio async support
    • No sidecar needed (pure Rust, like SFTP)
    • No #[cfg] guards (works on all platforms)
  2. FtpProvider implementing LocationProvider trait:

    • scheme()"ftp"
    • URL format: ftp://user@hostname:port/path (port defaults to 21)
    • All trait methods: read_directory, get_file_metadata, create_directory, delete, rename, copy, move_item
    • Cross-provider paste helpers: download_file_from_ftp(), upload_file_to_ftp(), download_ftp_file_to_temp()
  3. Auth: FTP auth is simpler than SFTP — just username/password (no SSH keys or agent)

    • Passwords stored in OS keychain (service "marlin-ftp")
    • Server list in ~/.config/marlin/ftp-servers.json
    • Error sentinel: [FTP_NO_CREDENTIALS]
  4. Connection pool: FTP connections are cheaper than SSH but still worth pooling

    • Consider FTPS (FTP over TLS) support as well — suppaftp supports it
  5. Commands: get_ftp_servers, add_ftp_server, remove_ftp_server, test_ftp_connection, download_ftp_file, window lifecycle commands

  6. Cross-provider paste: All 6 arms (ftp↔file, ftp↔gdrive, ftp↔smb) + ftp↔sftp

  7. Thumbnails: src-tauri/src/thumbnails/generators/ftp.rs — same pattern as sftp.rs

Frontend

  1. Types: FtpServerInfo, FtpConnectInitPayload, FtpConnectSuccessPayload
  2. Store: ftpServers state, pendingFtpCredentialRequest, credential error detection
  3. Connect window: src/windows/FtpConnectWindow.tsx — simpler than SFTP (no auth method dropdown, just username/password)
  4. Sidebar: FTP servers in Network section alongside SMB and SFTP, with "Add FTP Server..." button
  5. Events: ftp-connect:init, ftp-connect:success
  6. main.tsx routing: view === 'ftp-connect' → FtpConnectWindow

Provider Registration

Register in src-tauri/src/locations/mod.rs REGISTRY (follow SFTP pattern at line ~38):

let ftp_provider: ProviderRef = Arc::new(FtpProvider::default());
map.insert(ftp_provider.scheme().to_string(), ftp_provider);

Technical Notes

  • Crate: suppaftp with async-native-tls or async-rustls feature for FTPS
  • FTP vs FTPS: Consider supporting both plain FTP and FTPS (FTP over TLS) — suppaftp handles both
  • Passive mode: FTP has active vs passive mode for data transfers; passive mode is almost always what you want behind NATs/firewalls
  • No server-side copy: Like SFTP, FTP has no server-side copy — must download + re-upload
  • Binary mode: Always use binary transfer mode (not ASCII) for file integrity
  • The SFTP provider is the blueprint — follow the same patterns for module structure, commands, window lifecycle, store integration, sidebar rendering, and cross-provider paste

Acceptance Criteria

  • ftp://user@host:port/path URLs work in the path bar
  • FTP connect window with hostname, port (default 21), username, password
  • Connection is tested before showing "Connected" (like SFTP)
  • FTP servers appear in Network sidebar section
  • Browse directories, view file metadata
  • Upload files (local → ftp)
  • Download files (ftp → local)
  • Cross-provider paste works (ftp↔gdrive, ftp↔smb, ftp↔sftp)
  • Screenshot paste to FTP directory
  • Thumbnails for FTP-hosted images
  • Auto-credential prompt when navigating to unknown FTP server
  • Connection timeout (15s) with clear error message
  • Credentials stored in OS keychain
  • Both frontend and backend build cleanly

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions