A Python implementation of the Internxt CLI for encrypted cloud storage with path-based operations, timestamp preservation, and a built-in WebDAV server.
This is an unofficial, open-source project and is not affiliated with, endorsed by, or supported by Internxt, Inc. It is a personal project built for learning and to provide an alternative interface. Use it at your own risk.
- ✅ Mount as a local drive: Access your Internxt Drive directly from Finder, File Explorer, or any WebDAV client.
- ✅ Cross-platform support: Works on Windows, macOS, and Linux.
- ✅ Stable and Compatible: Uses
waitress(recommended) orcherootfor the best client compatibility. - ✅ Server Choice: Force
waitressorcherootwith the--serverflag for debugging.
- ✅ Human-readable paths: Use
/Documents/report.pdfinstead of UUIDs. - ✅ Fuzzy Search: Instantly find files and folders with a fast, server-side
searchcommand. - ✅ Wildcard Find: Use
findwith patterns like*.pdf,report*, etc. - ✅ Tree visualization: See your folder structure at a glance.
- ✅ Path navigation: Browse folders like your local filesystem.
- ✅ Timestamp Preservation: Preserves original file modification/creation dates on
uploadanddownload-path. - ✅ Secure authentication: Login/logout with 2FA support.
- ✅ File operations: Upload, download with progress indicators.
- ✅ Stream from stdin (
rcat): Pipe data straight to a Drive file (rclone-rcatstyle) for unattended backups —mariadb-dump | xz | cli.py rcat /backups/db.xz. - ✅ Large files: Streaming encrypt/upload and decrypt/download keep memory bounded (a few MB) regardless of file size; files ≥ 100 MiB automatically use true multipart upload (30 MB parts, uploaded in parallel and each retried independently) for resilient transfers on slow/flaky connections. Parallel ranged downloads are available opt-in via
--ranged. - ✅ Resumable uploads: Interrupted multipart uploads (≥ 100 MiB) are checkpointed as parts complete — a failed part is first repaired in-session (the AES-CTR keystream is seekable, so only that part is re-encrypted and re-PUT), and if the process dies, re-running the same upload resumes it: already-uploaded parts are skipped (no official Internxt client can do this). Resume works until the presigned URLs expire, then falls back cleanly to a fresh upload. Opt out with
--no-resume. - ✅ Folder management: Create and organize folders.
- ✅ Zero-knowledge encryption: AES-256-CTR client-side encryption.
# Install dependencies
pip install -r requirements.txt
# (Recommended for WebDAV)
pip install waitress
# Login to your account
python cli.py login
# Mount your drive locally! (EASIEST WAY TO USE)
python cli.py webdav-start
# Or, use path-based commands
python cli.py list-path
python cli.py search "report"
python cli.py find /Documents "*.pdf"
python cli.py upload ./my-docs --target /Backups# Login with interactive prompts
python cli.py login
# Login non-interactively
python cli.py login --email user@example.com --password mypass --2fa 123456
# Check current user
python cli.py whoami
# Logout and clear credentials
python cli.py logoutWays to provide the password, most to least secure:
# 1. Pipe via stdin — never appears in shell history, argv, or the process list
printf '%s' "$MY_PASSWORD" | python cli.py login -e you@example.com --password-stdin
# 2. Environment variables (good for CI; avoid `export`-ing into your shell rc)
INTERNXT_EMAIL=you@example.com INTERNXT_PASSWORD="$MY_PASSWORD" python cli.py login
# 3. The -e/-p flags ⚠️ leak into shell history and `ps` — avoid on shared hostsFor 2FA in automation, pass the TOTP secret instead of a one-off code:
--tfa-secret (or INTERNXT_TFA_SECRET) auto-generates the 6-digit code.
Add --save-tfa-secret once to keep the secret in the keychain; later logins
and implicit auto-login then generate codes from it without the flag or env var:
python cli.py login -e you@example.com --password-stdin --tfa-secret "$SECRET" --save-tfa-secretImplicit auto-login. You don't strictly need to run login first: when no
valid session is stored, every command (e.g. rcat) auto-logs-in from
INTERNXT_EMAIL / INTERNXT_PASSWORD (+ INTERNXT_TFA_SECRET if 2FA is on).
This also kicks in transparently if a stored token has expired. Set
INTERNXT_DEBUG=1 to surface the internal auth trace (off by default; it goes
to stderr so it never pollutes piped output).
After login the session (which includes your mnemonic — the key to all your
files) is encrypted at rest in ~/.internxt-cli/.inxtcli (file 0600, dir 0700).
The encryption key is sourced in this order:
- OS keychain (macOS Keychain / Linux Secret Service / Windows Credential
Manager) via
keyring— a random per-install key; recommended. Install withpip install keyringif not already present. INTERNXT_CREDENTIALS_KEYenv var — a key you supply (handy for CI where there's no desktop keychain).- Legacy static key — obfuscation only; used as a last resort if neither of
the above is available. The
0600permission is your real protection here.
Set INTERNXT_NO_KEYRING=1 to skip the OS keychain (forces option 2/3). Legacy
credential files from older versions are read and upgraded automatically.
Choosing the keychain (macOS). By default the OS default keychain (usually
login) is used. To keep the CLI's secrets in a different keychain, save it once:
python cli.py config --set-keychain ~/Library/Keychains/work.keychain-db
python cli.py config --unset-keychain # back to the OS defaultThe choice is stored in ~/.internxt-cli/settings.json; INTERNXT_KEYCHAIN
overrides it per run. The keychain must be unlocked. After switching, log in
again — a session saved under the old keychain can't be decrypted.
Mount your Internxt Drive as a local disk.
# Start the WebDAV server (it will print the URL and credentials)
python cli.py webdav-start
# Start in the background
python cli.py webdav-start --background
# Force a specific server (e.g., cheroot for SSL)
python cli.py webdav-start --server cheroot
# Check if the server is running
python cli.py webdav-status
# Stop the server
python cli.py webdav-stop
# Show mount instructions for your OS
python cli.py webdav-mount
# Test if the server is responding correctly
python cli.py webdav-test
# Show full WebDAV configuration and paths
python cli.py webdav-config
# Show advanced debugging info
python cli.py webdav-debug
# Regenerate SSL certs
python cli.py webdav-regenerate-sslAfter starting, open your file manager (Finder/File Explorer) or Client (like CyberDuck) and connect to the server (e.g., http://localhost:8080) with username internxt and password internxt-webdav.
# List root folder with readable paths
python cli.py list-path
# List specific folders
python cli.py list-path "/Documents"
python cli.py list-path "/Photos/2023/Summer"
# Show detailed information (size, date)
python cli.py list-path "/Documents" --detailed
# Show folder structure as tree
python cli.py tree
python cli.py tree "/Projects" --depth 2# Fast, global, server-side fuzzy search
python cli.py search "report"
# Show full details (size, date, full path) (slow!)
python cli.py search "report" --detailed
# Slow, client-side wildcard find (POSIX-like syntax, slow!)
python cli.py find / "*.pdf" # All PDF files in entire drive
python cli.py find /Photos "*.jpg" # All JPGs in /Photos
python cli.py find . "report*" # Files starting with "report" in current pathUpload
# Upload a single file, preserving its timestamp
python cli.py upload ./local-report.pdf --target /Documents --preserve-timestamps
# Upload a whole folder recursively, preserving all timestamps
python cli.py upload ./my-project --target /Backups --preserve-timestamps
# Upload with filters and overwrite conflicts
python cli.py upload ./photos --target /Photos --include "*.jpg" --on-conflict overwrite
# Legacy shorthand is also accepted: final remote-looking argument becomes the target
python cli.py upload ./photos /Photos
# Windows: use forward slashes for Internxt paths; quote local paths with spaces
python cli.py upload "D:/alle fotos" --target "/Katharina" --on-conflict skipStream from stdin (rcat)
# Pipe a stream straight to a Drive file (rclone-rcat style) — great for
# unattended backups without a named local file. REMOTE_PATH includes the
# filename; the parent folder is created if missing.
mariadb-dump mydb | xz -6 | python cli.py rcat /backups/mydb.xz
tar czf - /etc | python cli.py rcat /backups/etc.tar.gz
# Auto-login: if you're not logged in, rcat (and the other commands) log in
# from INTERNXT_EMAIL / INTERNXT_PASSWORD (+ INTERNXT_TFA_SECRET for 2FA), so a
# pipeline needs no separate `login` step:
INTERNXT_EMAIL=you@example.com INTERNXT_PASSWORD="$PW" \
pg_dump -Fc mydb | python cli.py rcat /backups/mydb.dmp
# ...or keep them separate and trap each phase:
python cli.py login -e you@example.com --password-stdin <<<"$PW" \
&& PGPASSWORD="$DBPW" pg_dump -Fc mydb | python cli.py rcat /backups/mydb.dmp \
&& python cli.py logout
# Upstream passwords: while piping into rcat the dump tool has no terminal, so
# it can't prompt for ITS OWN db password — supply it non-interactively, e.g.
# PGPASSWORD / ~/.pgpass (Postgres) or MYSQL_PWD / ~/.my.cnf (MySQL/MariaDB).
# Note: Internxt needs the exact size up front, so rcat first spools stdin to a
# temp file to measure it, then encrypts+uploads in one pass. Ensure free temp
# space for the whole stream (use --temp-dir to relocate the spool).Download
# Download a file by path, preserving its timestamp
python cli.py download-path -p "/Documents/report.pdf"
# Download a folder recursively to a local directory
python cli.py download-path -r "/Photos/2023" --destination ./My-Photos
# Download a folder with filters
python cli.py download-path -r "/Music" --include "*.mp3" --exclude "demo_*"
# Opt in to parallel ranged downloads for large files (≥ 100 MiB). Off by
# default; falls back to a single stream if the server ignores HTTP Range.
python cli.py download-path -p "/Backups/bigdump.xz" --ranged --chunk-workers 4# Move to trash by path
python cli.py trash-path "/OldDocuments/outdated.pdf"
python cli.py trash-path "/TempFolder"# Permanently delete by path (with warnings)
python cli.py delete-path "/TempFile.txt"# List folders (old way with UUIDs)
python cli.py list
python cli.py list --folder-id <folder-uuid>
# Create folders
python cli.py mkdir "My New Folder"
# Upload/Download by UUID (see path-based commands for more features)
python cli.py upload ./document.pdf
python cli.py download <file-uuid># Show current configuration
python cli.py config
# Test CLI components
python cli.py test
# Extended help with examples
python cli.py help-extended
# Debug path resolution
python cli.py resolve "/Documents/report.pdf"# Clone the repository
git clone https://github.com/CrispStrobe/internxt-python
cd internxt-python
# Install dependencies
pip install -r requirements.txt
# For the best WebDAV experience, install 'waitress'
pip install waitress # (Highly recommended for WebDAV)
# Start using immediately
python cli.py login
python cli.py webdav-start- Python 3.8 – 3.14
- Dependencies:
cryptography,mnemonic,tqdm,requests,click,WsgiDAV - WebDAV Server:
waitress(recommended) orcheroot - OpenPGP login keys are generated by a built-in backend (uses
cryptography; no extra dependency, all Python versions). Optional alternatives are auto-detected if installed:PGPy(Python < 3.13 only) orpython-gnupg(needs a systemgpgbinary) —pip install '.[pgpy]'/'.[gnupg]'.
This CLI implements the same security model as official Internxt clients:
- Client-side encryption: All files encrypted on your device before upload (AES-256-CTR).
- Zero-knowledge: Internxt servers never see your unencrypted data or keys.
- Secure Credentials: Encrypted and stored locally in
~/.internxt-cli/.
internxt-python/
├── cli.py # Main CLI interface with all commands
├── config/
│ └── config.py # Configuration management
├── services/
│ ├── auth.py # Authentication & login
│ ├── crypto.py # Encryption/decryption (AES-256-CTR)
│ ├── drive.py # Drive operations & path resolution
│ ├── network_utils.py # SSL cert lifecycle, range parsing
│ ├── webdav_provider.py # WsgiDAV provider for Internxt
│ └── webdav_server.py # WebDAV server management
├── utils/
│ └── api.py # HTTP API client
├── tests/ # Pytest suite (622 tests, 90% coverage)
├── pyproject.toml # Pytest, coverage, ruff config
├── requirements-dev.txt # Dev/test dependencies
└── .github/workflows/ci.yml # Lint + type-check + test on Py 3.10/3.11/3.12
# Clone and setup development environment
git clone https://github.com/CrispStrobe/internxt-python.git
cd internxt-python
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# Install in development mode
pip install -e .
pip install -r requirements-dev.txtThe project ships with a 591-test unit suite at 90% line coverage (plus 31 optional live tests — 622 total). It covers crypto round-trips, path resolution, upload/download conflict handling, the WebDAV provider, all major CLI commands, and a real encrypt → upload → "wire" → download → decrypt round-trip cycle.
# Run the full test suite (~3 seconds)
pytest
# With coverage report
pytest --cov=services --cov=utils --cov=config
# Run a specific test file
pytest tests/test_crypto.py -v
# In-CLI smoke check (no test framework needed)
python cli.py testPer-module coverage:
| Module | Coverage |
|---|---|
services/auth.py |
100% |
services/crypto.py |
100% |
utils/api.py |
98% |
services/webdav_provider.py |
91% |
services/network_utils.py |
90% |
services/drive.py |
89% |
config/config.py |
85% |
services/webdav_server.py |
83% |
| Total | 90% |
| File | Purpose |
|---|---|
HISTORY.md |
What's been done — full audit summary + every bug found and fixed during the test build-out. |
PLAN.md |
What's left — roadmap of unimplemented work: confirmed gaps vs. upstream internxt/cli, potential differentiators (folder copy, quota), WebDAV-providers reliability test, known maintenance debt. |
LEARNINGS.md |
Lessons carried forward — what each audit tool actually catches, why unit tests miss certain bugs, safety patterns for live tests against real accounts. |
tests/test_live_smoke.py is a 31-test suite that runs end-to-end
against the real Internxt backend, covering: login, list, upload (small
- unicode + extensionless + 2 MB), download with byte-for-byte verification, recursive folder creation, file rename/move/copy/update, folder rename/move, trash, server-side fuzzy search, and client-side wildcard find. Auto-skipped unless credentials are present.
# Put creds in a .env file (gitignored — never committed)
echo 'IXT_ACCOUNT=you@example.com' > .env
echo 'IXT_PWD=your-password' >> .env
# Runs in ~60-90 seconds, always cleans up
pytest tests/test_live_smoke.py -vAll operations happen inside a unique sentinel folder
(/__pytest_internxt_cli_smoke__/<run-uuid>/) which is always trashed
at teardown — your real files are never touched. Every file/folder name
within a test includes a UUID suffix so transient retries never
collide. Transient API failures (rate-limit, eventual-consistency) are
auto-retried via pytest-rerunfailures.
The CI runs four gates on every push/PR:
ruff check . # lint
mypy --no-incremental cli.py services # types
bandit -r . -x ./.mypy_cache,./__pycache__,./.git,./tests -ll # security (medium+)
pytest --cov=services --cov=utils --cov=config # testsAll four must pass.
python cli.py --help
python cli.py help-extended
python cli.py <command> --helpAGPL-3.0 license
Made with ❤️ for the Internxt community