← Back to projects

CLI tool · Python · open source

mftp

A command-line FTP client that says what it is doing. Recursive uploads and deletes, a sync that distinguishes uploading from replacing from skipping, and a password that never has to appear in your shell history.

Python 3.6+ colorama ftplib argparse setuptools Cross-platform

No screenshots on this page — it is a terminal program, so every block below is the real output, reproduced from the strings in ftp_client.py.

Install

zsh
$ pip install mftp_client-1.0.0-py3-none-any.whl
Successfully installed mftp-client-1.0.0 colorama-0.4.6

# then, from anywhere
$ mftp -H ftp.example.com -u myuser
The password flag is the one you should not use. Omitting -p makes the client prompt instead, which keeps the password out of your shell history — and out of the process list. The help text says so, and puts the recommended form first.

Connecting

mftp — session start
$ mftp -H ftp.example.com -u deploy
============================================================
FTP Client - Type 'help' for available commands
============================================================

Password for deploy@ftp.example.com:
Connecting to ftp.example.com:21 with username deploy...
✓ Connected successfully!
220 ProFTPD Server ready.

ftp> pwd
/home/deploy

Sync, and the three things it can do

sync replaces what is already there; sync-skip leaves it alone. Each path reports which of the three branches it took, so a deploy log tells you what actually changed.

Decision tree for sync and sync-skip on each path local path file or folder exists remotely? file_exists() no Uploading yes which command? sync ↻ Replacing sync-skip ⊘ Skipped a folder recurses, creating remote directories as it goes

sync — replace what is already there

mftp — sync
ftp> sync ./dist
Directory exists: dist
↻ Replacing: ./dist/index.html -> dist/index.html
✓ Synced: ./dist/index.html -> dist/index.html
Created remote directory: dist/assets
↑ Uploading: ./dist/assets/app.css -> dist/assets/app.css
✓ Synced: ./dist/assets/app.css -> dist/assets/app.css
↑ Uploading: ./dist/assets/app.js -> dist/assets/app.js
✓ Synced: ./dist/assets/app.js -> dist/assets/app.js
✓ Synced folder: ./dist -> dist

sync-skip — leave what is already there

mftp — sync-skip
ftp> sync-skip ./dist
Directory exists: dist
⊘ Skipped (exists): ./dist/index.html -> dist/index.html
⊘ Skipped (exists): ./dist/assets/app.css -> dist/assets/app.css
↑ Uploading: ./dist/assets/vendor.js -> dist/assets/vendor.js
✓ Synced: ./dist/assets/vendor.js -> dist/assets/vendor.js
✓ Synced folder: ./dist -> dist

Removing things, including directories

rm takes as many paths as you give it, notices when one is a directory, and recurses rather than failing.

mftp — rm
ftp> rm old.log cache.tmp legacy/
Removing 3 item(s)...
✓ Removed file: old.log
✓ Removed file: cache.tmp
'legacy/' is a directory, removing recursively...
✓ Removed file: legacy/2023/notes.txt
✓ Removed directory: legacy/2023
✓ Removed directory: legacy/
✓ Removed 3/3 item(s)

When it goes wrong

Failures are counted, not swallowed. A batch reports how many of its items actually succeeded.

mftp — failure modes
ftp> upload report.pdf
✗ Not connected to server

ftp> connect ftp.example.com deploy hunter2
Connecting to ftp.example.com:21 with username deploy...
✗ Connection failed: 530 Login authentication failed

ftp> upload missing.pdf report.pdf
Uploading 2 file(s)...
✗ Local file not found: missing.pdf
✓ Uploaded: report.pdf -> report.pdf
✓ Uploaded 1/2 file(s)

ftp> quit
✓ Disconnected successfully

The full command set

mftp — help
ftp> help
============================================================
FTP Client - Available Commands
============================================================

Connection:
  connect <host> <user> <password> [port]  - Connect to FTP server
  disconnect                                - Disconnect from server
  quit, exit                                - Exit the program

Navigation:
  ls [path]                                 - List directory contents
  cd <path>                                 - Change directory
  pwd                                       - Print working directory

File Operations:
  rm <file1> [file2] ...                    - Remove one or more files/directories
  rmdir <dir1> [dir2] ...                   - Remove one or more directories recursively
  upload <file1> [file2] ...                - Upload one or more files
  uploaddir <dir1> [dir2] ...               - Upload one or more folders recursively
  sync <path1> [path2] ...                  - Sync files/folders (upload new, replace existing)
  sync-skip <path1> [path2] ...             - Sync files/folders (upload new, skip existing)
  rename <old_name> <new_name>              - Rename file or folder

Other:
  help                                      - Show this help message

Flags

-H, --hostFTP server hostname or IP address.
-u, --userFTP username. Host and user are both required to connect on startup.
-p, --passwordNot recommended — will prompt if omitted. Passwords with |, &, ; or $ need single quotes if you insist on passing them.
-P, --portFTP port. Defaults to 21.
Runs where Python runs. macOS on Intel and Apple Silicon, every Linux distribution, Windows 10 and 11 — anything with Python 3.6 or newer. The only dependency is colorama, which is what makes the output above readable on Windows too.
mftp — built by MilanDroid · v1.0.0 All projects