8.3 KiB
Zero-Bandwidth Duplicati API
Thin Node.js + Express metadata service for Duplicati restores. The backend owns only SQLite metadata and enhancement jobs; file bytes stay on WebDAV and are never proxied through the runtime restore APIs.
What changed
- Multiple uploaded Duplicati task databases can now coexist at the same time.
- Every uploaded task library becomes its own
sourceId. - A single uploaded
Duplicati-server.sqlitecan be stored separately and used to map friendly task names onto task database files. - Directory browsing and file metadata APIs are now
sourceId-scoped. - The frontend is now split into three pages:
/login.htmlfor first-run setup and sign-in/for source management/source.html?sourceId=...for a source workbench/file.html?sourceId=...&id=...for video preview and browser-direct downloads
- Video preview and downloads now pull encrypted dblocks directly from WebDAV in the browser, decrypt them locally, and never proxy the file stream through the backend.
- The backend can build an enhanced SQLite copy per source by fetching remote
*.dblock.zip.aesfiles, decrypting them, and writing:archive_entry_indexvolume_crypto_cachevolume_scan_inventoryenhancement_meta
Endpoints
GET /api/sourcesGET /api/sources/:sourceIdPOST /api/sources/uploadGET /api/auth/statePOST /api/auth/setupPOST /api/auth/loginPOST /api/auth/logoutGET /api/system/tlsPUT /api/system/tlsPOST /api/system/tls/uploadDELETE /api/system/tls/custom-certificateGET /api/server-dbPOST /api/server-db/uploadPUT /api/sources/:sourceId/secretsPOST /api/sources/:sourceId/enhanceGET /api/sources/:sourceId/enhance/statusGET /api/ls?sourceId=...&path=/&snapshot=latestGET /api/file-info?sourceId=...&id=...GET /healthz
Compatibility routes:
POST /api/source/uploadGET /api/source/current
/api/source/current only works when the system currently contains exactly one source. If there are multiple sources, it returns 409 SOURCE_ID_REQUIRED.
Quick start
npm install
copy .env.example .env
npm start
Environment variables:
PORT: HTTP port, default3000HTTPS_PORT: HTTPS port, default3443APP_DB_PATH: service-owned SQLite path, default./data/app.sqliteUPLOAD_DIR: source storage root, default./data/sourcesTLS_DIR: certificate storage root, default./data/tlsPREVIEW_MAX_BYTES: max file size flagged as safe for in-memory preview, default16777216AUTH_SESSION_TTL_DAYS: login session lifetime in days, default30
Authentication
The system now requires username/password login before any source-management or restore API can be used.
- On first run, open
/login.htmland create the initial administrator account. - After setup, the protected pages
/,/source.html, and/file.htmlredirect unauthenticated users back to the login page. - API routes return:
401 AUTH_SETUP_REQUIREDwhen no administrator exists yet401 AUTH_REQUIREDwhen there is no valid session401 SESSION_EXPIREDwhen a saved session has expired
Implementation notes:
- credentials are stored in the service-owned
app.sqlite - passwords are hashed with Node's built-in
scrypt - sessions are stored server-side in SQLite and issued as
HttpOnlycookies - the same cookie is intentionally usable over both HTTP and HTTPS, so it is not forced to
Secure /healthzstays public so Docker/Jenkins health checks do not need to log in
HTTPS and certificates
The service now starts both listeners at the same time:
- HTTP on
PORT - HTTPS on
HTTPS_PORT
There is no forced redirect. You can open either protocol directly.
TLS behavior:
- if no custom certificate is configured, the app automatically generates and keeps a self-signed certificate so HTTPS always works
- the self-signed certificate is regenerated from the configured primary domain and SAN list when you save self-signed TLS settings
- custom certificates can be configured from the homepage either by pasting PEM text or by uploading PEM files
- deleting the custom certificate switches the service back to the generated self-signed certificate immediately
Certificate files are stored under TLS_DIR, while certificate metadata and the active mode are stored in app.sqlite.
Docker deployment note
Expose both ports when running the container:
3000for HTTP3443for HTTPS
Example:
docker run --rm -p 3000:3000 -p 3443:3443 duplicati-preview
Multi-source model
Each upload is stored under its own directory:
data/sources/<sourceId>/raw.sqlitedata/sources/<sourceId>/enhanced.sqlitedata/sources/<sourceId>/enhanced.sqlite.tmpdata/sources/<sourceId>/work/*
Duplicate uploads are deduplicated by SHA-256. If the same task database is uploaded again, the service returns the existing sourceId instead of creating a second copy.
The optional Duplicati-server.sqlite upload is stored separately under the upload root and never appears in /api/sources. It is only used to resolve friendly task names by matching path.basename(Backup.DBPath) against each uploaded task database filename.
When a server DB is available:
- source cards prefer
Backup.NameasdisplayName displayNameSourcebecomesserver-dbmatchedBackupNamereturns the matched task name
If no match exists, the source keeps using its original filename as displayName.
Upload model
There are now two upload paths:
- Task DB upload:
POST /api/sources/upload - Server DB upload:
POST /api/server-db/upload
Use the task DB upload for random-name Duplicati job databases such as TMQRJYNADS.sqlite.
Use the server DB upload only for Duplicati-server.sqlite. Uploading that file to the task DB endpoint is rejected with 422 INVALID_SOURCE_SCHEMA.
Enhancement flow
Raw Duplicati task databases can browse files immediately, but /api/file-info stays gated until archive indexes exist.
To enhance a source:
- Upload a raw task database.
- Save WebDAV base URL, auth mode, username/password, and backup passphrase.
- Start enhancement for that
sourceId. - The backend serially downloads each
*.dblock.zip.aes, decrypts the AES Crypt v2 container, scans the ZIP central directory, and writes supplemental tables into an enhanced SQLite copy. - After success, the source flips to
readyand/api/file-infostarts returning ordered segments and dblock mappings.
Current runtime assumptions:
- WebDAV auth supports
basicandanonymous - AES enhancement currently supports AES Crypt v2 volumes
- enhancement jobs run serially in-process
- credentials are stored in
app.sqliteand are not returned by the API
If enhancement fails, source browsing still works, and /api/file-info returns 409 ENHANCEMENT_FAILED.
Frontend restore flow
The runtime restore path stays "thick frontend, thin backend":
source.htmlbrowses the selected source with/api/lsfile.htmlfetches/api/file-info- the browser asks WebDAV directly for the required
*.dblock.zip.aes - the browser decrypts AES Crypt v2 locally
- the browser caches plain ZIP volumes in OPFS
- a root-scope
Service Workerserves pseudo-Range responses for<video> - downloads write restored bytes directly to disk with
showSaveFilePicker()
Important constraints:
- the backend still does not proxy file bytes
- browser-side WebDAV credentials are entered separately on
file.html - the current v1 range model fetches each needed dblock as a whole encrypted file, then decrypts it in the browser
- remote random access is therefore "logical range over cached dblocks", not true O(1) random access inside
.aes
Supported browser target:
- desktop Chromium with
Service Worker,OPFS,IndexedDB,showSaveFilePicker(),Web Crypto, andDecompressionStream
Supplemental schema
The enhancement job writes the SQL shape described in sql/supplemental-schema.sql.
If you already have a pre-enhanced SQLite file, uploading it is also supported. In that case the source is immediately marked ready.
Runtime note
This implementation uses Node 24's built-in node:sqlite module. In this Windows environment, native sqlite3 compilation is not reliable enough, so the service uses the built-in adapter instead.