d0c7ab61a7
CI / compile (pull_request) Successful in 4s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 24s
build / build (push) Successful in 7s
CI / compile (push) Successful in 5s
CI / unit (push) Successful in 22s
CI / integration (push) Successful in 24s
FILE_SHARING.md explains how the feature works and assumes you already know the node story; there was nothing describing what a fresh box has to provide. SHARE_NODE_SETUP.md fills that: the four host directories, the uid-33 readability requirement on the media library, deploying the musician+share stack (Portainer or CLI), forwarding /share from the reverse proxy, and the musician-side CONJURER_SHARE_* wiring - each with its verification command. Calls out the three couplings that actually break it in practice: the media must be mounted at the SAME container path on both containers (index paths and symlinks are absolute), the link/index dirs must be shared, and CONJURER_SHARE_BASE_URL must match the real public URL - it is not set in the bundled stack file and silently falls back to czernobog.pl. Also records what the node does NOT need: no host Apache, no cron, no Python, no hand-placed scripts - Apache, the vhost, scan_shares.py and revoke_shares.py are all baked into the image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
150 lines
5.8 KiB
Markdown
150 lines
5.8 KiB
Markdown
# Share node setup — from zero
|
|
|
|
Everything the share service needs is **inside the image**: Apache, the vhost,
|
|
the index scanner and the link revoker. The node needs no Apache, no cron, no
|
|
Python and no copies of the old hand-placed scripts.
|
|
|
|
What the node actually provides is four directories, the media library, and a
|
|
way in from the internet.
|
|
|
|
> Feature docs (how links work, security model, TTL semantics) live in
|
|
> [FILE_SHARING.md](FILE_SHARING.md). This file is only "how to stand it up on a
|
|
> fresh box".
|
|
|
|
## What runs where
|
|
|
|
| Piece | Where it lives | Notes |
|
|
|---|---|---|
|
|
| Apache + vhost | in the image | rendered from `share-vhost.conf.tpl` at start |
|
|
| `scan_shares.py` (index) | in the image | sleep loop, not cron |
|
|
| `revoke_shares.py` (expiry) | in the image | sleep loop, not cron |
|
|
| the symlinks | host `/srv/share/links` | **created by the musician**, served here |
|
|
| the index | host `/srv/share/db` | written here, **read by the musician** |
|
|
| TLS / public name | your reverse proxy | container speaks plain HTTP |
|
|
|
|
The single most important fact: **the musician creates the links, this service
|
|
serves them.** They must see the same directories, at the same paths.
|
|
|
|
## 1. Directories
|
|
|
|
```bash
|
|
sudo mkdir -p /srv/share/{media,links,db,logs}
|
|
```
|
|
|
|
| Path | Contents | Mounted as |
|
|
|---|---|---|
|
|
| `/srv/share/media` | the media library | `/mnt/shares` (read-only) |
|
|
| `/srv/share/links` | published symlinks | `/var/www/html/share` |
|
|
| `/srv/share/db` | `share_scan.json` | `/srv/share/db` |
|
|
| `/srv/share/logs` | Apache logs incl. `share_access.log` | `/var/log/apache2` |
|
|
|
|
## 2. Media library and permissions
|
|
|
|
Put the library at `/srv/share/media` (bind mount, NFS mount, whatever — it is
|
|
only ever read). Apache serves as **`www-data`, uid 33 inside the container**, so
|
|
that uid must be able to traverse and read it:
|
|
|
|
```bash
|
|
sudo chmod -R o+rX /srv/share/media # simplest; or use ACLs/group instead
|
|
sudo -u '#33' test -r /srv/share/media/<some-file> && echo "readable by www-data"
|
|
```
|
|
|
|
A library that root can read but uid 33 cannot is the classic "every link
|
|
404s / 403s" cause.
|
|
|
|
## 3. Deploy the stack
|
|
|
|
The musician and the share service share a filesystem, so the supported layout
|
|
is **both on the same node**, from one stack:
|
|
|
|
- Portainer → **Stacks → Add stack** → paste `docker/compose.musician-share.stack.yaml`
|
|
- or CLI: `docker compose -f docker/compose.musician-share.stack.yaml up -d`
|
|
|
|
That stack already wires the four mounts on both containers. Set
|
|
`CONJURER_SHARE_SERVER_NAME` to your public hostname.
|
|
|
|
Share-only node (musician elsewhere): use `docker/compose.share.yaml` and put
|
|
`/srv/share/links` + `/srv/share/db` on storage **both** hosts mount — otherwise
|
|
the musician cheerfully creates links this container cannot see.
|
|
|
|
## 4. Way in from the internet
|
|
|
|
The container listens on **8081 → 80**, plain HTTP by design; TLS stays on the
|
|
reverse proxy you already run. Forward the `/share/` prefix **unchanged** — the
|
|
links are `https://<host>/share/<token>`:
|
|
|
|
nginx:
|
|
```nginx
|
|
location /share/ {
|
|
proxy_pass http://<share-node-ip>:8081/share/;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
```
|
|
|
|
Apache (as reverse proxy):
|
|
```apache
|
|
ProxyPass /share/ http://<share-node-ip>:8081/share/
|
|
ProxyPassReverse /share/ http://<share-node-ip>:8081/share/
|
|
```
|
|
|
|
Then: DNS for the public name points at the proxy, and the node's firewall lets
|
|
the proxy reach 8081 (nothing else needs to).
|
|
|
|
## 5. Tell the musician where to publish
|
|
|
|
The musician builds the URLs it posts to Discord. In its env:
|
|
|
|
```ini
|
|
CONJURER_SHARE_DIR=/var/www/html/share
|
|
CONJURER_SHARE_DB=/srv/share/db/share_scan.json
|
|
CONJURER_SHARE_BASE_URL=https://czernobog.pl/share
|
|
```
|
|
|
|
`CONJURER_SHARE_BASE_URL` is **not** set in the bundled stack file — it falls back
|
|
to `https://czernobog.pl/share`. If your public name differs, set it explicitly
|
|
or every posted link points at the wrong host.
|
|
|
|
## The three couplings that break it
|
|
|
|
1. **Same container path for the media.** The index records absolute paths and
|
|
the symlinks are absolute. Both containers must mount the library at
|
|
`/mnt/shares`. Mount it elsewhere on one side and every link dangles.
|
|
2. **Same link dir and index dir** for musician and share (same host, or shared
|
|
storage).
|
|
3. **`CONJURER_SHARE_BASE_URL` must equal your real public `/share` URL.**
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
# index built (should be non-trivial JSON)
|
|
sudo head -c 200 /srv/share/db/share_scan.json; echo
|
|
|
|
# the two jobs and Apache are alive
|
|
docker logs conjurer-share | tail -20 # "[share] serving ... scan every ...s"
|
|
|
|
# directory listing MUST fail (403) - it would leak every live token
|
|
curl -sI http://<share-node-ip>:8081/share/ | head -1
|
|
|
|
# revoker state files must NOT be served
|
|
curl -sI http://<share-node-ip>:8081/share/.downloads.json | head -1
|
|
|
|
# a real link: publish one from Discord, then
|
|
curl -sI https://<public-host>/share/<token> | head -1 # 200
|
|
```
|
|
|
|
After the first download of a link, `revoke_shares.py` removes it once
|
|
`CONJURER_SHARE_TTL_SECONDS` (default 1h) has passed. A link nobody downloads is
|
|
never revoked by that job.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
|---|---|---|
|
|
| every link 404 | media mounted at a different container path than `/mnt/shares`, or the symlink target is gone | align the mounts on both containers |
|
|
| every link 403 | media not readable by uid 33 | step 2 |
|
|
| links created but not served | musician and share not sharing `/srv/share/links` | step 3 |
|
|
| `/get_share_list` empty | index missing/not shared | check `/srv/share/db/share_scan.json` and the musician's `CONJURER_SHARE_DB` |
|
|
| links never expire | revoker cannot see the access log, or nobody downloaded them | check `/srv/share/logs/share_access.log` exists and grows |
|
|
| wrong host in posted links | `CONJURER_SHARE_BASE_URL` | step 5 |
|