114 lines
5.4 KiB
Markdown
Executable File
114 lines
5.4 KiB
Markdown
Executable File
# 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.
|