# XFER — copyparty file server behind Cloudflare Tunnel
This folder (`/Volumes/4TB/XFER`) is served online by **copyparty**, exposed through a
**cloudflared** named tunnel at **https://v01.rasik.life**.
Everything copyparty needs is contained in `.server/` (hidden from visitors; also
shadowed in the config so it is never served).
## Live architecture
```
browser → https://v01.rasik.life (Cloudflare edge, TLS terminated there)
→ cloudflared named tunnel "xfer" (id 3150c84b-44fc-4ad3-8c81-32b3c54dbf09)
→ copyparty on http://127.0.0.1:3923 (loopback only, no open ports)
```
## Components
| What | Where | Notes |
|---|---|---|
| copyparty sfx | `.server/copyparty-sfx.py` | self-contained, run with system `python3` (3.14, homebrew) |
| copyparty config | `.server/copyparty.conf` | **all settings live here** — accounts, permissions, UI flags |
| IMAX CRT theme (enabled) | `.server/ui/` (`imax-crt.css`, `imax-crt.js`, `VT323-Regular.ttf`; old `amber-crt.*` files kept but unused) | amber-phosphor CRT control-panel theme inspired by IMAX projection booth. **Enabled by default** — toggled on/off via a button fixed top-right (state in `localStorage` key `imax_crt`). IMAX 1.43:1 letterbox frame is a separate toggle (key `imax_frame`). CSS is linked via `html-head `, JS loaded via `js-browser`. Old `amber-crt.css`/`amber-crt.js` are superseded — do not re-enable them |
| index db / thumbnails | `.server/hist/` | kept out of the shared tree on purpose |
| logs | `.server/copyparty.log`, `.server/cloudflared.log` | appended on each start |
| start/stop scripts | `.server/start.sh`, `.server/stop.sh` | start.sh launches copyparty + named tunnel if not running |
| quick-tunnel config | `.server/quicktunnel.yml` | intentionally empty; keeps quick tunnels from inheriting `~/.cloudflared/config.yml` credentials (causes 404s) |
| cloudflared launchd agent | `~/Library/LaunchAgents/life.rasik.v01.cloudflared.plist` | starts tunnel at login, KeepAlive; logs to `~/.cloudflared/cloudflared.log` |
| login-item launcher app | `~/Applications/XFERFileServer.app` | runs `.server/start.sh` at login (copyparty can't be a launchd agent — see TCC note) |
| cloudflared config | `~/.cloudflared/config.yml` | ingress: `v01.rasik.life → http://127.0.0.1:3923` |
| cloudflared cert + tunnel creds | `~/.cloudflared/cert.pem`, `~/.cloudflared/3150c84b-*.json` | secrets; cert is scoped to the rasik.life zone |
## Access model (current policy)
- **Anonymous visitors** are auto-redirected from `/` to `/ANON/` by an inline
`html-head` script (checks for `"acct":"admin"` in the DOM; admin stays at root).
Inside `/ANON/` they get `rw` (browse, download, upload, mkdir). Everything
outside `/ANON/` is invisible to them (root listing shows only the ANON folder;
direct file URLs elsewhere return 403).
- **admin: full access** (`A`) everywhere — root + ANON. Username/password in
`.server/copyparty.conf` → `[accounts]`. Login via the key button in the UI,
or `?pw=user:pass` URL param.
- Volumes: `[/]` is `A: admin` only; `[/ANON]` is `rw: *` + `A: admin`;
`[/.server]` is `//NULL` (never served).
- To restrict anon to read-only (no upload/mkdir), change `rw: *` to `r: *` in
`[/ANON]`. To allow anon upload without browsing, change to `w: *`.
## Operating
```bash
# start everything (idempotent)
/Volumes/4TB/XFER/.server/start.sh
# stop everything
/Volumes/4TB/XFER/.server/stop.sh
# health checks
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3923/ # local
curl -s -o /dev/null -w "%{http_code}\n" https://v01.rasik.life/ # public
pgrep -fl "copyparty-sfx|cloudflared tunnel run" # processes
```
After editing `.server/copyparty.conf`: `stop.sh && start.sh` (config reload only
covers non-`[global]` sections; a full restart is safer).
## Persistence across reboots
- **cloudflared**: launchd agent `life.rasik.v01.cloudflared` (RunAtLoad + KeepAlive).
All its files live in `~`, so launchd can run it. Manage with `launchctl
bootstrap/bootout/kickstart gui/$(id -u)/life.rasik.v01.cloudflared`.
- **copyparty**: login item `~/Applications/XFERFileServer.app` runs `start.sh` at
login. It is NOT a launchd agent on purpose: macOS TCC blocks launchd-spawned
processes from removable volumes (`/Volumes/4TB` → "Operation not permitted",
exit 78 with empty logs). The app gets a one-time TCC prompt ("allow access to
removable volumes") — it was already opened once; if macOS ever re-asks, click Allow.
- Both mechanisms fire at **login**, not boot. For true headless operation enable
automatic login (System Settings → Users & Groups), or convert to LaunchDaemons
+ Full Disk Access grants (not configured).
- **start.sh waits for the drive**: the script blocks up to 120s for
`/Volumes/4TB/XFER/.server/copyparty-sfx.py` to appear before proceeding, so
login-item launch races (drive not yet mounted) no longer cause a silent
failure. If the drive doesn't mount within 120s, the script logs an error and
exits non-zero instead of failing silently.
## Temporary quick tunnel (optional, disposable)
For a random public URL without touching DNS:
```bash
cloudflared tunnel --config /Volumes/4TB/XFER/.server/quicktunnel.yml --url http://127.0.0.1:3923
```
URL is printed at startup, changes every run, dies with the process. Always pass the
empty `--config` file (see table above).
## DNS / registrar facts (hard-won, don't rediscover)
- Domain `rasik.life` is registered at **Hostinger**; nameservers are
`ns1.dns-parking.com` / `ns2.dns-parking.com` (Hostinger's own DNS).
**Do NOT switch nameservers to Cloudflare** — rasik.life hosts other apps
(PHOTOBooth, FHM, etc.) on Hostinger that would break. Only the `v01`
subdomain routes through Cloudflare via a CNAME record on Hostinger's DNS zone.
- The `v01` CNAME record on Hostinger's DNS zone points to
`3150c84b-44fc-4ad3-8c81-32b3c54dbf09.cfargotunnel.com` (the Cloudflare
tunnel). This is the ONLY record that touches Cloudflare. All other records
(rasik.life A, www, ftp, neon, dsl, lib) stay on Hostinger.
- There is an *older* Cloudflare zone for the same domain on
`harlee.ns.cloudflare.com` / `vick.ns.cloudflare.com` (different account, unknown).
If DNS ever flips to those, the tunnel record will vanish from the internet.
- DNSSEC is **off** at the registry — keep it off unless DS records are updated,
or the zone will break.
- The Cloudflare tunnel DNS route exists in Cloudflare's zone too (for when the
domain was briefly on Cloudflare NS). Recreate if ever lost:
`cloudflared tunnel route dns xfer v01.rasik.life`
- Hostinger API: base `https://developers.hostinger.com` (use this — `api.hostinger.com`
has intermittent 530s/Cloudflare 1016). Bearer token auth, endpoints under
`/api/domains/v1/` (portfolio, nameservers) and `/api/dns/v1/` (zone records).
**A Hostinger API token was pasted in chat on 2026-07-25; treat it as exposed and
rotate it in the Hostinger panel.**
- Hostinger Python SDK (`pip install hostinger_api`) is installed in a venv at
`.server/hostinger/.venv` but has a pydantic enum case-sensitivity bug (API returns
"Active", SDK expects "active"). Use curl against the REST API instead.
- To fix DNS if v01 breaks: (1) verify Hostinger NS are set to dns-parking via
`GET /api/domains/v1/portfolio/rasik.life`, (2) verify the v01 CNAME via
`GET /api/dns/v1/zones/rasik.life`, (3) if missing, add CNAME v01 → tunnel cfargotunnel.com
via `PUT /api/dns/v1/zones/rasik.life`. See `.server/hostinger/fix_dns.sh` (needs updating
to use the CNAME approach, not the nameserver approach).
## copyparty flags currently enabled (`.server/copyparty.conf`)
- `e2dsa` + `e2ts`: file index, search, unpost, media tags (ffprobe)
- `usernames`: username+password login dialog
- `grid`, `gsel`: gallery view + multiselect by default (good on mobile)
- `theme: 2`, `name: v01.rasik.life`, `no-robots`
- `html-head`: single line containing `` to `/ui/imax-crt.css` (theme CSS)
+ inline `