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
CONNECTIONS_PER_DOWNLOAD 4 Initial value: parallel connections for one download when the server allows ranges
ALLOWED_PORTS 80,443 Initial value: allowed remote ports, * for any
LOG_LEVEL info debug, info, warn or error. debug also logs every polling request
LOG_FORMAT text text or json

Logging

Logs go to stdout (docker compose logs -f). Every request gets an X-Request-ID. When the server hits an internal error, the page shows a reference ID and the log line for that request carries the same request_id, so the two can be matched. Download, extraction and move steps are logged with a short download ID.

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

  1. Create the download folder first, so Docker does not create it as root:
    mkdir -p data/downloads
    
  2. .env is preset for testing: downloads go to ./data/downloads, cookies work over plain HTTP. Both destination drives must be mounted before starting.
  3. Start and open http://127.0.0.1:8080 (user admin, password from .env):
    docker compose up -d --build
    

Deployment

  1. Make sure PUID/PGID can write to the download folder and to the first level folders of both destination roots.
  2. Copy and edit the environment file:
    cp .env.example .env
    
  3. Start the service:
    docker compose up -d --build
    
  4. 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.
S
Description
No description provided
Readme
125 KiB
0 Stars 1 Watchers 0 Forks
Languages
Go 64.1%
JavaScript 21.4%
CSS 8.5%
HTML 5.8%
Dockerfile 0.2%