Design
A plugin-based universal command-line downloader. Resumable, multi-connection accelerated, self-healing — everything is a plugin.
- Package / command:
downctl - Repository: https://github.com/ThzxxArt/downctl
- License: MIT · Python: 3.11+
1. Positioning
downctl downloads files reliably from almost anywhere. It is not tied to a
single site: HuggingFace, GitHub Releases, S3, SFTP and local files are all
just built-in source plugins, using exactly the same interface as
third-party plugins.
Everything is a plugin.
2. The core/plugin boundary
"Everything is a plugin" needs to distinguish two kinds of thing, otherwise a stateful, tightly-coupled algorithm gets shattered into slow, fragile pieces.
2.1 The plugin layer (replaceable)
All strategy lives behind one interface, and the built-ins use that same interface (dogfooding):
| Category | Responsibility | Built-ins |
|---|---|---|
| Source | Resolve input into tasks | url, huggingface, github, s3, sftp, file |
| Transport | Transfer bytes | http, sftp, file |
| Verifier | Check digests | sha256 |
| Sink | Persist bytes | filesystem |
| Reporter | Progress & logs | tqdm, json, quiet |
| Router | Rewrite URLs | mirror |
| Hook | Lifecycle events | logfile, archive, webhook, manifest, exec, checksum |
The Sink contract is defined and the
filesystemsink is implemented; the core currently performs ranged writes directly to the target file, so sinks are available to plugins but not on the engine's hot path.
2.2 The core (stable, not a plugin)
Stateful algorithms stay in a stable core, exposed to plugins as API:
- scheduling and concurrency control
- the resumable piece bitmap
- the stall watchdog
- rate limiting
- file locking
- disk-space pre-flight
The core is the engine; the plugins are the gears. We don't turn the engine into gears.
3. Layout
downctl/
├── src/downctl/
│ ├── cli.py, app.py, config.py, session.py, probe.py, console.py, platform_utils.py
│ ├── core/ # stable engine (not plugins)
│ │ ├── engine.py transfer.py pieces.py watchdog.py
│ │ ├── ratelimit.py locks.py task.py context.py errors.py
│ ├── plugin/ # registry + loader + event bus + protocols
│ └── builtin/ # built-in plugins (via the plugin interface)
│ ├── sources/{url,huggingface,github,s3,sftp,file}, _sigv4
│ ├── transports/{http,sftp,file}, _sftp
│ ├── verifiers/, sinks/, reporters/, routers/, hooks/
├── tests/{unit, integration}/
├── docs/ (bilingual site)
├── examples/plugins/
└── .github/workflows/{ci, docs, release, codeql}.yml
4. Plugin system
4.1 Discovery (highest priority first)
- Local directories:
./downctl_plugins/,~/.config/downctl/plugins/ - Installed packages advertising a
downctl.*entry-point - Built-ins:
downctl.builtin
4.2 Selection
- Route by URI scheme:
https://→http,hf://→huggingface,gh://→github,s3://→s3,sftp://→sftp,file://→file. --sourceforces a specific plugin.- Several candidates are ranked by
priority+can_handle(target).
4.3 API versioning
downctl.API_VERSION (currently "1"). A plugin declares the version it was
built against; the registry rejects mismatches.
4.4 Transports carry the protocol
A transport returns a streaming Response (status, headers, chunk iterator)
and maps its native errors onto the downctl exception hierarchy, so the core
never depends on a specific HTTP library.
5. Resumable downloads
- A download is split into ~8 MiB pieces; a single file uses up to
connectionsin parallel. - A piece bitmap (
<file>.downctl-pieces) is the authoritative ledger of completed pieces. Multi-connection completion is out of order, so file size alone cannot describe resume state — the bitmap can. - On interruption the data file is
fsync-ed, then the bitmap is written atomically; a retry re-fetches only missing pieces. - The bitmap records its piece size (v3), so changing
--connections/--piece-sizereslices completion state by byte range instead of discarding progress. - Reporters can seed a resume-aware bar: the engine computes already-complete
bytes from the bitmap and calls the optional
run_resumed/task_resumedhooks. - Without a bitmap, a full-size file is trusted only if it provably has no
holes (SEEK_HOLE), the NTFS sparse flag is clear, or the user opts in with
DOWNCTL_TRUST_PARTIAL=1.
6. CLI
downctl get <target>… download (source auto-selected by scheme)
downctl probe <target>… size/status only
downctl status [DIR] incomplete/failed downloads
downctl clean [DIR] remove leftover bitmaps / failed log / locks
downctl plugin list|info plugin management
downctl config init|show|path
downctl doctor environment diagnostic
downctl wizard interactive
downctl version
7. Configuration
~/.config/downctl/config.toml (override with DOWNCTL_CONFIG), with
${VAR} interpolation. Plugin options live under [plugins.<name>].
See Configuration for the full key list.
8. Packaging & CI
- Build backend: hatchling (PEP 621), version dynamic from
src/downctl/__init__.py. - CI: lint (ruff/mypy) + matrix tests (ubuntu/macos/windows × 3.11–3.13,
coverage ≥ 85%) + build/twine-check +
pip-audit. - Release:
release.ymlonv*tags publishes to PyPI via Trusted Publishing (OIDC) and creates a GitHub Release. - Docs:
docs.ymlbuilds this site and deploys to GitHub Pages.
9. Roadmap
Done: resumable core, plugin framework, url/hf/github/s3/sftp/file sources, http/sftp/file transports, hooks, CLI, bilingual docs, Docker, PyPI release.
0.1.2 (polish): resume-aware progress, resliceable bitmaps, --force /
--skip-existing, stdin targets, --piece-size / --min-split-size,
probe --json, completions, exec / checksum hooks, GitHub private-asset
downloads, richer verbose output.
Optional next: richer reporter (rich), Homebrew / standalone binaries, additional sources, sink backends wired onto the hot path.