S3Duck π¦
August 19, 2026 Β· View on GitHub
Simple cross-platform GUI client for S3-compatible object storage (AWS S3, MinIO, Ceph, and others).

Continuous integration
GitHub Actions (.github/workflows/ci.yml) runs three jobs on every push and
pull request:
- tests β the offscreen suite on Python 3.11 and 3.12, against both the
pinned PyQt6 from
requirements.txtand the newest wheel. The same code has behaved differently on a wheel, a pinned older wheel and Debian'spython3-pyqt6, so more than one Qt is covered on purpose. - icons without the Qt SVG plugin β strips PyQt6's SVG icon engine to
reproduce Debian/Mint's
python3-pyqt6, then requires every icon to still render from its bundled PNG twin. This is the environment that produced blank toolbar buttons. - .deb package β builds the package and checks that the build substituted
every control placeholder, that
Installed-Sizeis measured rather than hardcoded, that the Qt SVG/PDF dependencies are declared, and that a PNG twin ships for every bundled SVG.
Features
- Multi-profile management β create, edit, copy, and delete named connection profiles; credentials are encrypted at rest using Fernet symmetric encryption. Each row shows its endpoint, region and pinned bucket, with colour-coded
[read-only]/[TLS unverified]badges beside the name, and the list is keyboard-driven (Enteropens,F2edits,Delremoves). Connecting runs off the UI thread behind a cancellable dialog, so an unreachable endpoint no longer freezes the launcher - Profile export / import β move profiles between machines as a bundle whose credentials are encrypted with a passphrase you choose (never written in the clear)
- Read-only profiles β mark a profile read-only to block every write and delete; the toolbar and context menus hide mutating actions, the title bar shows
[read-only], and the data layer refuses writes as a backstop - Temporary credentials β session-token support for STS / SSO / assumed-role / MFA keys, with one-click import of any profile from
~/.aws/credentials - Icons that survive a minimal Qt β the toolbar prefers your desktop icon theme, but falls back to bundled art whenever the theme has no entry or paints nothing visible; each bundled SVG has a PNG twin, so icons still appear on a Qt build without the SVG plugin (Debian/Mint's
python3-pyqt6ships none). Runpython3 tools/icon_report.pyto see what your desktop resolves - Per-profile accent colour β mark a profile with a colour; the launcher row and the open window both carry it, so prod is distinguishable from dev at a glance
- Diagnostics (Tools β Diagnosticsβ¦) β Qt plugin availability, how every icon resolves, library versions and the active transfer settings, in one copyable report
- Quick open (
Ctrl+P) β type-to-jump across buckets and bookmarks, using the command palette's matcher - Sync to another profile β compare this prefix against one in a different account or provider, review the dry-run plan, then run it; the bytes stream through this machine because no server-side copy can span two credentials
- Command palette (
Ctrl+K) β type-to-run index of every available action, built from the live actions so it cannot drift - Copy to another profile β stream objects (or whole prefixes) into a different account or provider; a server-side copy cannot use two sets of credentials, so the bytes travel through this machine
- Bucket browser β list, create, and delete buckets; recursive delete runs through the transfer queue (progress + cancel, UI stays responsive)
- Empty bucket β delete every object, version, delete marker and in-flight upload while keeping the bucket, queued with progress and cancel
- Incomplete uploads β find and abort in-flight multipart uploads that are invisible to normal listings but keep their parts billed; shows how much space they waste and can abort everything older than N days
- Object browser β navigate prefixes as a virtual folder tree with sorting by name, size, and modified date; optional Storage-class and ETag columns via the header context menu (both come free with the listing)
- Upload β single/multiple files via dialog or drag-and-drop from the OS file manager; whole directory trees via "Upload folder" (
Ctrl+Shift+U) or drag-and-drop - Download β single files or entire folder prefixes, recreating the directory tree locally
- Download as ZIP β stream a selection (files and whole folders) straight into one archive, without staging it on disk twice
- Drag out β drag objects from the list onto a file manager; the selection is downloaded to a temp folder first, with progress and a size warning. Staged payloads are removed when the app exits, and a crashed run's leftovers are reclaimed on the next start
- Resumable uploads β a large upload is sent part by part with its upload id recorded, so an interrupted transfer resumes instead of restarting; a cancelled one deliberately leaves the parts on the server (clean them up from Incomplete uploads)
- Resumable downloads β large files download as parallel ranges into a
.s3duckpartfile with a progress sidecar, so an interrupted transfer picks up where it stopped instead of restarting (a changed ETag discards the stale partial) - Additional checksums β upload with CRC32/SHA1/SHA256; CRC32 is requested as a whole-object checksum so multipart objects stay verifiable, unlike a multipart ETag
- Checksum verification β optionally compare each downloaded file against the object's stored digest and fail the transfer on a mismatch; a full-object checksum is preferred over the ETag, which cannot be checked for multipart objects
- Parallel transfers β configurable number of files moving at once and multipart connections within each file; applies to uploads, downloads (including whole prefixes) and sync
- Transfer settings β files in flight, connections per file, multipart part size and threshold, resumable uploads, plus the storage class, checksum and server-side encryption (SSE-S3 / SSE-KMS) applied to uploads, persisted across sessions
- Overwrite protection β uploads (dialog, folder upload and drag-in), downloads (single files and whole folders), copies, moves, renames and pastes all detect existing destinations and offer Skip / Overwrite / Cancel
- Sync with a local folder (
Ctrl+E) β compare a directory against a prefix in either direction, review a dry-run plan (upload / download / delete / skip with a reason per file), then run it through the queue; supports exclude globs (*.tmp,node_modules/) and optionally deleting extras at the destination - Transfer queue β queued jobs with per-row progress, cancel, and retry for failed or cancelled entries
- Transfer history β a persisted log of past jobs (when, what, bytes, outcome) with one-click re-run for small jobs, from the queue panel
- Bandwidth limit β optional ceiling on total transfer throughput, shared across every parallel file and chunk
- Completion notifications β a desktop notification when the queue drains while the window is in the background (toggle in Transfer settings)
- Delete β objects and folder prefixes (recursive, batched 1000 keys per call); confirmation shows the scanned object count and total size; recursive bucket delete also purges noncurrent versions, delete markers and in-flight uploads
- Undo delete (
Ctrl+Z) β on a versioning-enabled bucket a delete only writes a delete marker, so the last delete can be rolled back by removing those markers - Clipboard (
Ctrl+C/Ctrl+X/Ctrl+V) β copy or cut a selection and paste it into any folder or bucket; copying also puts thes3://URIs on the system clipboard for use elsewhere - Copy / Move β server-side copy or move of a multi-selection, within a bucket or across buckets; when the destination lives in another region or account (where a server-side copy is impossible) the object is streamed through instead of failing
- Rename β in-place rename of a file or folder (server-side copy + delete), on the context menu or
F2 - Bulk rename (
Shift+F2) β rename a whole selection by find-and-replace (optionally regex, with backreferences) or a{name}/{ext}/{n}numbering template, with a live preview and duplicate/invalid-name checks - Create folder β creates an S3 prefix placeholder
- Preview / open β double-click a file to preview images, text, PDFs and syntax-highlighted code in-app, with a hex dump for binaries; anything can still be opened with the OS default application
- Recursive search β search a whole bucket/prefix by key substring or regular expression (
Ctrl+Shift+For "Search hereβ¦"), filtered by size range, extension and modified date, with jump-to-location on any result - Object versioning β enable/suspend bucket versioning from the UI; list every version and delete marker of an object, download a specific version, promote an older version to current, or delete individual versions
- Storage class β view an object's storage class and change it (Standard, IA, Intelligent-Tiering, Glacier, Deep Archive, β¦); works on a multi-selection or whole folders and runs through the transfer queue
- Glacier restore β initiate a restore of archived objects (single, multi-select, or whole prefixes) with a chosen retrieval tier and retention window, queued like other transfers; restore status shown in properties
- Bulk tagging β add, overwrite or strip tags across a whole selection, expanding folders to every object beneath them
- Edit metadata β set
Content-Type,Cache-Control,Content-Disposition,Content-Encoding, and customx-amz-meta-*user metadata - Object properties β key, size, ETag, storage class, restore status, and public URL
- Presigned links β generate a temporary download (GET) or upload (PUT) link with a configurable expiry (up to the 7-day S3 maximum)
- Make public β set
public-readACL and copy direct URL; when the ACL is refused, Block Public Access and Object Ownership settings are reported as the reason - Clickable breadcrumb β jump straight to any parent prefix, the bucket root, or the bucket list from the path bar
- Go to location (
Ctrl+L) β paste ans3://bucket/prefix(or a bare prefix) and jump straight there, across buckets - Restore last location β each profile reopens the bucket and prefix you left it in, and the launcher preselects the profile you used last
- Bookmarks (
Ctrl+B) β save any bucket/prefix and return to it from the toolbar menu, with rename/remove management; stored per profile - Remembered layout β splitter position, column widths and sort order persist between sessions
- Listing summary β folder/file counts and total size of the current listing in the status bar
- Keyboard shortcuts (
Ctrl+/) β a searchable reference generated from the app's own actions, since plain letters are reserved for type-to-search - Theme β Light, Dark, or system-default appearance, remembered across sessions
- Duplicate finder (Tools β Find duplicates,
Ctrl+Shift+D) β group objects holding identical content by size + ETag at the cost of a listing, showing reclaimable space; select all-but-newest/oldest and delete the redundant copies. Objects whose ETags cannot settle the question (multipart vs single-part) are listed separately and never auto-selected - Bucket usage stats β total size and object count, breakdown by file category with a pie chart, top folder groups, a storage-class breakdown and the largest objects
- Runtime profile switch β switch S3 accounts without restarting the app
- Cached bucket bindings β the proven endpoint/region/addressing combination per bucket is remembered, so reopening an off-region bucket skips the probe round trips
- Automatic region/endpoint detection β when an operation fails due to a region or endpoint mismatch the app probes the server for the correct region, rebuilds the client, and retries transparently; applies to bucket open, listing, upload, download, and delete
- S3-compatible storage β path-style addressing option for MinIO and similar backends
- Cross-platform β Linux, macOS, Windows
Planned work and known limitations are tracked in ROADMAP.md.
Requirements
| Dependency | Version | Purpose |
|---|---|---|
| Python | β₯ 3.10 | Runtime |
| PyQt6 | β₯ 6.7 | GUI framework |
| boto3 | β₯ 1.42 | AWS / S3-compatible SDK |
| cryptography | β₯ 46.0 | Fernet credential encryption |
| pyinstaller | β₯ 6.18 | Binary packaging (optional) |
Running from Source
Quick start (system packages, Debian/Ubuntu):
sudo apt install python3-boto3 python3-cryptography python3-pyqt6
python3 s3duck.py
Recommended β virtualenv:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 s3duck.py
Building
Debian / Ubuntu package
sudo apt-get install git devscripts build-essential lintian upx-ucl
./build_deb.sh # auto-detects amd64 / arm64
./build_deb.sh arm64 # explicit architecture
Output: build/s3duck_<version>_<arch>.deb
Linux binary (PyInstaller)
./build_linux_bin.sh
macOS binary + DMG
./build_macos_bin.sh # native arch
./build_macos_bin.sh universal2 # fat binary (x86_64 + arm64)
./build_dmg.sh
Windows binary
build_win.cmd
Pre-built releases are available on the GitHub releases page.
Project Architecture
s3duck/
βββ s3duck.py Entry point β QApplication bootstrap, Profiles dialog
βββ main_window.py Main window β file browser, toolbar, async workers
βββ model.py S3/data layer β all boto3 operations, region retry logic
βββ settings.py Profile create/edit dialog
βββ properties_window.py Object properties dialog
βββ profile_switcher.py Runtime profile-switch dialog
βββ theme.py Light / Dark / system palette switching
βββ utils.py Shared helpers (str_to_bool, center_on_screen, ~/.aws parsing, local tree scan)
β
βββ icons/ 24 px SVG icons for toolbar and context menus
βββ resources/ App icon (ico/icns/png), screenshots, .desktop file
βββ DEBIAN/ Debian package metadata (control, postinst, prerm)
β
βββ requirements.txt Python dependencies
βββ s3duck.spec PyInstaller build spec
βββ build_deb.sh Build .deb package
βββ build_linux_bin.sh Build Linux self-contained binary
βββ build_macos_bin.sh Build macOS self-contained binary
βββ build_dmg.sh Pack macOS binary into .dmg
βββ build_win.cmd Build Windows self-contained binary
Layers
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Entry / Profile layer s3duck.py β
β Profiles dialog, Crypto (Fernet), SettingsItem β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β UI layer main_window.py β
β MainWindow, Tree, UpTopProxyModel, PieWidget, β
β BucketUsageDialog, ListItem β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Worker / Threading layer main_window.py β
β NavigationWorker BucketEnterWorker Worker UsageWorker β
β (each runs in a QThread, communicates via pyqtSignal) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Dialog layer β
β SettingsWindow PropertiesWindow ProfileSwitchWindow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Data / S3 layer model.py β
β Model β boto3 wrapper, adaptive region/endpoint probing β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key components
| Component | File | Responsibility |
|---|---|---|
Profiles | s3duck.py | CRUD for connection profiles; launches MainWindow |
Crypto | s3duck.py | Fernet encrypt/decrypt of stored credentials |
MainWindow | main_window.py | Root window β toolbar, splitter (tree + log), statusbar |
Tree | main_window.py | Drag-and-drop QTreeView; hands drops to upload worker |
UpTopProxyModel | main_window.py | Proxy that pins [..] to top and sorts BUCKET < FOLDER < FILE |
NavigationWorker | main_window.py | Off-thread bucket/prefix listing; uses a private Model clone to avoid client races |
BucketEnterWorker | main_window.py | Off-thread bucket entry with hints-based region/endpoint retry |
Worker | main_window.py | Off-thread upload / download / delete with byte-level progress and cancellation |
UsageWorker | main_window.py | Off-thread bucket size aggregation by file category |
PieWidget | main_window.py | Custom QPainter pie chart for usage breakdown |
Model | model.py | All boto3 calls; _try_bind_bucket probes addressing styles; rebind_bucket auto-corrects region mid-session |
PreviewDialog | main_window.py | In-app image/text preview; "open with default app" via a temp download |
VersionsDialog | main_window.py | Per-object version manager (list / download / make-current / delete); lists off-thread |
IncompleteUploadsDialog | main_window.py | Find and abort orphaned multipart uploads still holding billed parts |
TransferSettingsDialog | main_window.py | Concurrency plus upload storage class / encryption |
OverwriteDialog | main_window.py | Skip / Overwrite prompt for existing destinations |
ShortcutsDialog | main_window.py | Keyboard reference derived from the live QActions |
TransferHistoryDialog | main_window.py | Past transfers with re-run |
RateLimiter | model.py | Shared token bucket capping total throughput |
BulkTagsDialog | main_window.py | Add / replace / remove tags across a selection |
BookmarksDialog | main_window.py | Rename / remove saved locations |
DuplicateFinderDialog | main_window.py | Size+ETag duplicate scan with guarded deletion |
CodeHighlighter | main_window.py | Language-agnostic syntax highlighting in previews |
BulkRenameDialog | main_window.py | Find-replace / template rename with live preview |
SyncDialog | main_window.py | Localβremote comparison, dry-run plan, queued execution |
MetadataDialog | main_window.py | Edit Content-Type / caching headers and custom user metadata |
SearchDialog | main_window.py | Recursive key search over a bucket/prefix with jump-to-location |
PresignedLinkDialog | main_window.py | Generate GET/PUT presigned links with a configurable expiry |
Breadcrumb | main_window.py | Clickable path bar for jumping to parent prefixes |
apply_theme | theme.py | Light / Dark / system palette switching |
SettingsWindow | settings.py | Profile form (name, URL, region, bucket, keys, flags) |
PropertiesWindow | properties_window.py | Object metadata: key, size, ETag, storage class, restore status, public URL |
ProfileSwitchWindow | profile_switcher.py | Runtime profile switch without app restart |
Data flow
User action
β
βΌ
MainWindow ββspawnβββΊ QThread + Worker/NavigationWorker
β (private Model clone or shared Model)
β
βΌ
Model.method()
β boto3 S3 API call
βΌ
AWS S3 / MinIO / Ceph β¦
β
pyqtSignal (progress / finished / error)
β
βΌ
MainWindow ββupdateβββΊ UI (tree, log, progress bar)
Region / endpoint auto-retry flow
Operation fails (AuthorizationHeaderMalformed | PermanentRedirect)
β
βΌ
get_bucket_hints() HEAD Bucket β x-amz-bucket-region header
β
βΌ
build_region_swapped_endpoint() rewrite AWS endpoint for new region
β
βΌ
rebind_bucket() swap endpoint + region β enter_bucket() β validate
β
βΌ
retry original operation transparent to the caller
Credential storage
New profile
β access_key, secret_key
βΌ
Crypto.encrypt() (Fernet, key stored in QSettings "common/key")
β encrypted bytes
βΌ
QSettings β ~/.config/s3duck/s3duck.ini
Launch profile
β encrypted bytes from QSettings
βΌ
Crypto.decrypt() β plaintext creds β boto3.Session
License
See LICENSE.
Author
Vladislav Ananev Β© 2022β2026