S3Duck πŸ¦†

August 19, 2026 Β· View on GitHub

Simple cross-platform GUI client for S3-compatible object storage (AWS S3, MinIO, Ceph, and others).

ListBuckets Screenshot


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.txt and the newest wheel. The same code has behaved differently on a wheel, a pinned older wheel and Debian's python3-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-Size is 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 (Enter opens, F2 edits, Del removes). 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-pyqt6 ships none). Run python3 tools/icon_report.py to 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 .s3duckpart file 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 the s3:// 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+F or "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 custom x-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-read ACL 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 an s3://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

DependencyVersionPurpose
Pythonβ‰₯ 3.10Runtime
PyQt6β‰₯ 6.7GUI framework
boto3β‰₯ 1.42AWS / S3-compatible SDK
cryptographyβ‰₯ 46.0Fernet credential encryption
pyinstallerβ‰₯ 6.18Binary 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

ComponentFileResponsibility
Profiless3duck.pyCRUD for connection profiles; launches MainWindow
Cryptos3duck.pyFernet encrypt/decrypt of stored credentials
MainWindowmain_window.pyRoot window β€” toolbar, splitter (tree + log), statusbar
Treemain_window.pyDrag-and-drop QTreeView; hands drops to upload worker
UpTopProxyModelmain_window.pyProxy that pins [..] to top and sorts BUCKET < FOLDER < FILE
NavigationWorkermain_window.pyOff-thread bucket/prefix listing; uses a private Model clone to avoid client races
BucketEnterWorkermain_window.pyOff-thread bucket entry with hints-based region/endpoint retry
Workermain_window.pyOff-thread upload / download / delete with byte-level progress and cancellation
UsageWorkermain_window.pyOff-thread bucket size aggregation by file category
PieWidgetmain_window.pyCustom QPainter pie chart for usage breakdown
Modelmodel.pyAll boto3 calls; _try_bind_bucket probes addressing styles; rebind_bucket auto-corrects region mid-session
PreviewDialogmain_window.pyIn-app image/text preview; "open with default app" via a temp download
VersionsDialogmain_window.pyPer-object version manager (list / download / make-current / delete); lists off-thread
IncompleteUploadsDialogmain_window.pyFind and abort orphaned multipart uploads still holding billed parts
TransferSettingsDialogmain_window.pyConcurrency plus upload storage class / encryption
OverwriteDialogmain_window.pySkip / Overwrite prompt for existing destinations
ShortcutsDialogmain_window.pyKeyboard reference derived from the live QActions
TransferHistoryDialogmain_window.pyPast transfers with re-run
RateLimitermodel.pyShared token bucket capping total throughput
BulkTagsDialogmain_window.pyAdd / replace / remove tags across a selection
BookmarksDialogmain_window.pyRename / remove saved locations
DuplicateFinderDialogmain_window.pySize+ETag duplicate scan with guarded deletion
CodeHighlightermain_window.pyLanguage-agnostic syntax highlighting in previews
BulkRenameDialogmain_window.pyFind-replace / template rename with live preview
SyncDialogmain_window.pyLocal↔remote comparison, dry-run plan, queued execution
MetadataDialogmain_window.pyEdit Content-Type / caching headers and custom user metadata
SearchDialogmain_window.pyRecursive key search over a bucket/prefix with jump-to-location
PresignedLinkDialogmain_window.pyGenerate GET/PUT presigned links with a configurable expiry
Breadcrumbmain_window.pyClickable path bar for jumping to parent prefixes
apply_themetheme.pyLight / Dark / system palette switching
SettingsWindowsettings.pyProfile form (name, URL, region, bucket, keys, flags)
PropertiesWindowproperties_window.pyObject metadata: key, size, ETag, storage class, restore status, public URL
ProfileSwitchWindowprofile_switcher.pyRuntime 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