Automation — Cloud-First Downloads (Python)

The pattern in 30 seconds
  1. Log in with /auth/login to get a token.
  2. List your purchases / groups and their files.
  3. If a file has cloud_share_link, download the bytes from that Cloudflare URL — no token, much faster.
  4. Otherwise (brand-new files not yet on the CDN), fall back to /download/<uuid> with your token.

Two download paths — always try the cloud one first

PATH 1 - CLOUD (preferred)
    GET https://vault.dnfilevault.com/<uuid>
    - URL comes from the file record's "cloud_share_link" field
    - No Authorization header needed (the UUID in the URL is the key)
    - Served by Cloudflare edge near you: fast, no queuing

PATH 2 - ORIGIN (fallback)
    GET https://api.dnfilevault.com/download/<uuid>
    - Requires your Bearer token
    - Served from our origin server, which is shared by everyone
      and gets overwhelmed during peak hours

Rule: use PATH 1 whenever "cloud_share_link" is present.
      Use PATH 2 only when it is absent, or when PATH 1 fails.

Why it matters: in our own testing the same file took under 1 second from the CDN versus 19 seconds from the origin, and the gap widens on multi-gigabyte files. During busy periods the origin slows down further, while the CDN stays fast. If your scripts still pull everything from /download/, you are choosing the slow, congested path.

What the file listing returns

Both GET /purchases/<id>/files and GET /groups/<id>/files return records shaped like this. The cloud_share_link field is optional: it only appears once the background uploader has pushed the file to Cloudflare, so files published minutes ago may not have it yet.

{
  "uuid_filename": "fd217fd2-9c1e-4b7a-8f3d-2a6e5c8d1b04.zip",
  "display_name": "L3_20260805.zip",
  "file_size": 75123456,
  "created_at": "2026-08-05 16:45:00",
  "cloud_share_link": "https://vault.dnfilevault.com/fd217fd2-9c1e-4b7a-8f3d-2a6e5c8d1b04.zip"
}

Example A — minimal cloud-first download

The smallest correct client. Logs in, lists one group, downloads the newest file cloud-first with an origin fallback. Replace group id 4 with one of yours (GET /groups shows them all).

import os
import requests

BASE_URL = "https://api.dnfilevault.com"
UA = {"User-Agent": "DNFileVaultClient/1.0 ([email protected])"}

# 1) Log in once and keep the token
login = requests.post(
    f"{BASE_URL}/auth/login",
    json={
        "email": os.environ["DNFILEVAULT_EMAIL"],
        "password": os.environ["DNFILEVAULT_PASSWORD"],
    },
    headers=UA,
    timeout=60,
)
login.raise_for_status()
token = login.json()["token"]
auth = dict(UA, Authorization=f"Bearer {token}")

# 2) List the files in one of your groups (replace 4 with your group id)
files = requests.get(
    f"{BASE_URL}/groups/4/files", headers=auth, timeout=60
).json()["files"]

# 3) Download the newest file - CLOUD FIRST, origin as fallback
f = files[-1]
cloud_url = f.get("cloud_share_link")  # absent on brand-new files

if cloud_url:
    resp = requests.get(cloud_url, stream=True, timeout=300)  # no token needed
else:
    resp = requests.get(
        f"{BASE_URL}/download/{f['uuid_filename']}",
        headers=auth, stream=True, timeout=300,
    )
resp.raise_for_status()

with open(f["display_name"], "wb") as out:
    for chunk in resp.iter_content(chunk_size=1024 * 1024):
        if chunk:
            out.write(chunk)

print("saved", f["display_name"])

Example B — production-grade cloud-first downloader

Drop-in version with the behaviors a scheduled job needs: skips files already on disk at the correct size, streams to a .part file and renames only on success, refuses to retry on 401/403/410 (those mean expired entitlement — retrying never helps), and falls back from CDN to origin automatically.

import os
import requests

BASE_URL = "https://api.dnfilevault.com"
UA = {"User-Agent": "DNFileVaultClient/1.0 ([email protected])"}


def login() -> str:
    r = requests.post(
        f"{BASE_URL}/auth/login",
        json={
            "email": os.environ["DNFILEVAULT_EMAIL"],
            "password": os.environ["DNFILEVAULT_PASSWORD"],
        },
        headers=UA,
        timeout=60,
    )
    r.raise_for_status()
    return r.json()["token"]


def download_cloud_first(file_record, dest_dir, token):
    """
    Download one file record, cloud first.

    - If the record has a cloud_share_link, pull the bytes from the
      Cloudflare CDN (fast, no token needed).
    - Otherwise (or if the CDN fails), fall back to the origin endpoint
      /download/<uuid> with your Bearer token.
    - Streams to <name>.part and renames on success, so an interrupted
      transfer never leaves a file that looks complete.
    """
    name = file_record["display_name"]
    dest = os.path.join(dest_dir, name)
    part = dest + ".part"
    expected = file_record.get("file_size")

    if os.path.exists(dest):
        if expected is None or os.path.getsize(dest) == expected:
            print("skip (already have)", name)
            return dest

    urls = []
    cloud_url = file_record.get("cloud_share_link")
    if cloud_url:
        urls.append((cloud_url, {}))  # CDN: no auth header
    urls.append((f"{BASE_URL}/download/{file_record['uuid_filename']}",
                 {"Authorization": f"Bearer {token}"}))

    last_err = None
    for url, extra_headers in urls:
        try:
            with requests.get(
                url, headers={**UA, **extra_headers},
                stream=True, timeout=300,
            ) as resp:
                if resp.status_code in (401, 403, 410):
                    raise SystemExit(
                        f"Access problem for {name}: HTTP {resp.status_code} "
                        "(expired or no entitlement - retrying will not help)"
                    )
                resp.raise_for_status()
                with open(part, "wb") as out:
                    for chunk in resp.iter_content(chunk_size=1024 * 1024):
                        if chunk:
                            out.write(chunk)
            if expected and os.path.getsize(part) != expected:
                raise IOError(
                    f"size mismatch: got {os.path.getsize(part)}, expected {expected}"
                )
            os.replace(part, dest)  # atomic on same filesystem
            print("saved", name)
            return dest
        except SystemExit:
            raise
        except Exception as e:
            last_err = e
            print(f"attempt failed: {e} - trying next source")

    raise RuntimeError(f"all sources failed for {name}: {last_err}")


if __name__ == "__main__":
    token = login()
    auth = dict(UA, Authorization=f"Bearer {token}")
    os.makedirs("dnfilevault_data", exist_ok=True)

    # Mirror every group you belong to
    groups = requests.get(f"{BASE_URL}/groups", headers=auth, timeout=60).json()["groups"]
    for g in groups:
        files = requests.get(
            f"{BASE_URL}/groups/{g['id']}/files", headers=auth, timeout=60
        ).json()["files"]
        for f in files:
            download_cloud_first(f, "dnfilevault_data", token)

Credentials go in environment variables, never in the script:

Windows:   set [email protected]
           set DNFILEVAULT_PASSWORD=yourpassword

Linux/mac: export [email protected]
           export DNFILEVAULT_PASSWORD=yourpassword
Don't want to write code? Use the ready-made sync script

dnfilevault_sync_mirror.py is a single heavily-commented file that implements everything on this page: login, discovery of all purchases and groups, cloud-first downloads with automatic origin fallback, skip-if-present mirroring, and .part-file safety. Run it nightly from Task Scheduler or cron:

pip install requests
python dnfilevault_sync_mirror.py --dry-run                    # preview
python dnfilevault_sync_mirror.py --dir D:\MarketData\DNFileVault

Gotchas worth knowing

  • No cloud_share_link on a file? It was published very recently and the background uploader hasn't pushed it to Cloudflare yet. Fall back to /download/ for that file; the link appears on later listings.
  • 401 / 403 / 410 on /download/: your purchase or group membership has expired. Do not loop or retry — renew and try again.
  • Always send a descriptive User-Agent (like the one above). Blank or default user agents trip our anti-scanner protection and get deliberately slowed. This is not a ban.
  • Stream large files with stream=True and generous timeouts (300s), and write in chunks — never load a multi-GB zip into memory.
  • Don't re-download what you have. Compare file_size against the file already on disk and skip matches — the examples above both do this.