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
-
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)
-
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()
-
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]
-
Connection pool: FTP connections are cheaper than SSH but still worth pooling
- Consider FTPS (FTP over TLS) support as well —
suppaftp supports it
-
Commands: get_ftp_servers, add_ftp_server, remove_ftp_server, test_ftp_connection, download_ftp_file, window lifecycle commands
-
Cross-provider paste: All 6 arms (ftp↔file, ftp↔gdrive, ftp↔smb) + ftp↔sftp
-
Thumbnails: src-tauri/src/thumbnails/generators/ftp.rs — same pattern as sftp.rs
Frontend
- Types:
FtpServerInfo, FtpConnectInitPayload, FtpConnectSuccessPayload
- Store:
ftpServers state, pendingFtpCredentialRequest, credential error detection
- Connect window:
src/windows/FtpConnectWindow.tsx — simpler than SFTP (no auth method dropdown, just username/password)
- Sidebar: FTP servers in Network section alongside SMB and SFTP, with "Add FTP Server..." button
- Events:
ftp-connect:init, ftp-connect:success
- 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
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:
src-tauri/src/locations/sftp/mod.rs(SftpProvider, LocationProvider trait, URL parsing, cross-provider helpers)src-tauri/src/locations/sftp/auth.rs(keychain storage, server list in~/.config/marlin/sftp-servers.json)src-tauri/src/locations/sftp/pool.rs(session caching, 15s connect timeout, 5min idle eviction)src-tauri/src/commands.rs:7059-7190(get/add/remove servers, test connection, download, window lifecycle)src-tauri/src/commands.rs:2519-2716(sftp↔file, sftp↔gdrive, sftp↔smb)src-tauri/src/thumbnails/generators/sftp.rs(download to temp, generate locally)src/types/index.ts:283-303(SftpServerInfo, SftpConnectInitPayload, SftpConnectSuccessPayload)src/store/useAppStore.ts(sftpServers state, pendingSftpCredentialRequest, credential error detection)src/windows/SftpConnectWindow.tsx(form with hostname/port/username/auth method)src/components/Sidebar.tsx:745-809(server list in Network section, disconnect, "Add SFTP Server..." button)src/utils/events.ts:3-4(sftp-connect:init, sftp-connect:success)No FTP support exists currently.
Proposed Changes
Rust Backend
New module:
src-tauri/src/locations/ftp/withmod.rs,auth.rs,pool.rssuppaftpcrate with tokio async support#[cfg]guards (works on all platforms)FtpProvider implementing
LocationProvidertrait:scheme()→"ftp"ftp://user@hostname:port/path(port defaults to 21)read_directory,get_file_metadata,create_directory,delete,rename,copy,move_itemdownload_file_from_ftp(),upload_file_to_ftp(),download_ftp_file_to_temp()Auth: FTP auth is simpler than SFTP — just username/password (no SSH keys or agent)
"marlin-ftp")~/.config/marlin/ftp-servers.json[FTP_NO_CREDENTIALS]Connection pool: FTP connections are cheaper than SSH but still worth pooling
suppaftpsupports itCommands:
get_ftp_servers,add_ftp_server,remove_ftp_server,test_ftp_connection,download_ftp_file, window lifecycle commandsCross-provider paste: All 6 arms (ftp↔file, ftp↔gdrive, ftp↔smb) + ftp↔sftp
Thumbnails:
src-tauri/src/thumbnails/generators/ftp.rs— same pattern as sftp.rsFrontend
FtpServerInfo,FtpConnectInitPayload,FtpConnectSuccessPayloadftpServersstate,pendingFtpCredentialRequest, credential error detectionsrc/windows/FtpConnectWindow.tsx— simpler than SFTP (no auth method dropdown, just username/password)ftp-connect:init,ftp-connect:successview === 'ftp-connect'→ FtpConnectWindowProvider Registration
Register in
src-tauri/src/locations/mod.rsREGISTRY (follow SFTP pattern at line ~38):Technical Notes
suppaftpwithasync-native-tlsorasync-rustlsfeature for FTPSsuppaftphandles bothAcceptance Criteria
ftp://user@host:port/pathURLs work in the path bar