6027cfabb941527764a6dfa4a3e922b66f4067e9
DDC (Direct Download Client)
Self-hosted direct download client that fetches files onto the server, extracts archives, and moves the results into media folders.
Stack
- Backend: Go standard library only (no third-party dependencies), single static binary
- Frontend: plain HTML, CSS, and JavaScript modules embedded in the binary
- Container: distroless static image, non-root, read-only root filesystem
Project Layout
| Path | Purpose |
|---|---|
main.go |
Startup and graceful shutdown |
internal/config |
Environment configuration and library root parsing |
internal/preferences |
Settings editable from the web page, saved to .settings.json |
internal/libraries |
Discovers and validates first level library folders |
internal/auth |
Sessions, credential check, login rate limiting |
internal/guard |
URL validation and SSRF-safe HTTP client |
internal/fetch |
Resumable multi-connection downloads with progress, size and stall limits |
internal/history |
Move history saved to .history.json |
internal/extract |
Zip and tar extraction with Zip Slip and bomb protection |
internal/files |
Safe path resolution, file tree, cross-device moves |
internal/diskspace |
Free space checks |
internal/jobs |
Download job lifecycle, pause and resume, state saved to .job.json per download |
internal/server |
HTTP routes, middleware, handlers |
web/static |
Single page frontend |
deploy/nginx |
Reverse proxy example |
Destinations
Every first level folder inside these roots is a move destination:
/media/blob/TeaNet/Plex/media/blob/ChaiNet/Plex
Security
- Session login with HttpOnly, Secure, SameSite=Strict cookie
- Login lockout after 5 failed attempts per client for 15 minutes
- Same-origin check and JSON-only bodies on state changing requests
- SSRF protection: http/https only, allowed port list, every resolved IP checked at connect time (blocks loopback, private, link-local, CGNAT, metadata, ULA, NAT64, multicast), redirects revalidated, proxies ignored
- Zip Slip protection: absolute paths and
..entries rejected, symlinks, hardlinks and devices skipped - Extraction limited by free disk space and entry count
- Move targets restricted to first level folders of the destination roots (hidden folders and symlinks excluded), subfolders validated component by component, existing files never overwritten
- Disk space checked before download, during download, before extraction, and before cross-device copies
- Strict Content-Security-Policy, no inline scripts, all untrusted names rendered as text
Supported Archives
.zip, .tar, .tar.gz, .tgz, .tar.bz2, .tbz2, .tbz
Configuration
| Variable | Default | Description |
|---|---|---|
AUTH_USERNAME |
required | Login username |
AUTH_PASSWORD |
required | Login password, minimum 12 characters |
COOKIE_SECURE |
true |
Set false only for plain HTTP testing |
SESSION_TTL_HOURS |
12 |
Session lifetime |
TRUST_PROXY_HEADERS |
false |
Use X-Real-IP for login rate limiting (enable behind nginx) |
LISTEN_ADDR |
:8080 |
Listen address inside the container |
DOWNLOAD_DIR |
/data/downloads |
Temporary download directory |
MIN_FREE_GB |
10 |
Initial value: free space that must always remain |
MAX_CONCURRENT_DOWNLOADS |
2 |
Initial value: parallel downloads, others wait in queue |
ALLOWED_PORTS |
80,443 |
Initial value: allowed remote ports, * for any |
Web Settings
The Settings dialog edits parallel downloads, free space to keep, allowed ports, default extract and cleanup options, and the theme. Server settings are saved to .settings.json in the download folder and override the initial values above. The theme is stored per browser.
Local Testing
- Create the download folder first, so Docker does not create it as root:
mkdir -p data/downloads .envis preset for testing: downloads go to./data/downloads, cookies work over plain HTTP. Both destination drives must be mounted before starting.- Start and open
http://127.0.0.1:8080(useradmin, password from.env):docker compose up -d --build
Deployment
- Make sure
PUID/PGIDcan write to the download folder and to the first level folders of both destination roots. - Copy and edit the environment file:
cp .env.example .env - Start the service:
docker compose up -d --build - Install the nginx site:
sudo cp deploy/nginx/ddc.conf /etc/nginx/sites-available/ddc.conf sudo ln -s /etc/nginx/sites-available/ddc.conf /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx
Notes
- Moves are instant renames when the download directory and destination share a mount, otherwise files are copied and the source removed.
- Downloads left in the download directory after a restart are listed again as ready.
Languages
Go
64.1%
JavaScript
21.4%
CSS
8.5%
HTML
5.8%
Dockerfile
0.2%