Skip to content

Setup deep links open their page; Docker permission failures name the folder, the user and the fix - #381

Merged
KodeStar merged 15 commits into
mainfrom
fix/new-user-a
Nov 14, 2026
Merged

KodeStar merged 15 commits into
mainfrom
fix/new-user-a

Conversation

@KodeStar

Copy link
Copy Markdown
Owner

Two items from a new user's first install (Synology NAS, coming from Audiobookshelf), v2.7.0 row 15b items 1 and 2.

Setup deep links (closes #365)

The console's setup redirect ran on any page, and a new tab starts with empty sessionStorage, so "Open the import" in the Audiobookshelf step (and any bookmark) reopened setup instead of the linked page. The redirect now fires only from a plain visit to the Overview; the first page a session shows once setup state loads marks the session, so moving from a deep link to the Overview does not pull the admin back (admin-ui/src/features/setup).

Docker permission failures explain themselves (closes #368)

  • New internal/fsaccess: one test-write probe and one sentence naming the folder, the user the server runs as, the folder's owner and the fix (PUID/PGID to the owner, or give that user access).
  • Start-up checks the data folder and database files (a read-only config.yaml still starts); an unwritable backups folder warns at start.
  • Backups: a refused folder is permission_denied (it was reported as a generic failed), and the bell and notifications carry the sentence.
  • Scans: a library folder the server can reach but not list was scanned as fine with 0 books; it is now unavailable (offline), with the same wording.
  • docker-entrypoint.sh: after the chown, checks /data and the backups folder for writing and every other mount for reading as the server's user, warns and starts. Each mount check is bounded at 5 s and a share that does not answer says so (no owner advice). AUDIOSILO_BACKUP_DIR is now chowned (AudioSilo's own backups only, no symlinks followed). Refuses to chown a data path that resolves to / (//, /., /data/..).

Tests and verification

  • Go tests for fsaccess, backup, notify, scanner, launcher; admin-ui setup tests (a deep link never redirects; the new case fails on the old code); scripts/test-entrypoint.sh (Docker, 19 cases, not in CI).
  • Full gate green; reviewed with /simplify and /code-review (full level).
  • Verified live: setup deep links in Playwright (Open the import, the Reach step's HTTPS link, bookmarks, Overview after a deep link, plain /admin still goes to setup); the built image with 1026:100 volumes and chown refused (container log, start error, Backups page, failed backup in the console, bell and webhook, an unreadable library going offline). Docker Desktop bind mounts do not enforce Linux permissions, so a real NAS run is still owed.

Docs: KodeStar/audiosilo-docs PR to follow (Synology page, permissions troubleshooting). Site: KodeStar/audiosilo-site fix/new-user-a.

While setup was unfinished, any console page opened in a fresh browser
session was redirected to the flow, so the ABS step's "Open the import"
(a new tab, which starts its own session) reopened the same setup step
and the import was never reached. The same caught the Reach step's HTTPS
settings link, bookmarks and reloads.

The redirect now fires only from the Overview, where a plain visit to
/admin lands. The first page a session shows with the setup state loaded
marks the session either way, so moving on to the Overview from a deep
link does not pull the admin back into the flow.

Part of #365.
A data folder the server could not write to stopped the start with
SQLite's "unable to open database file" and nothing else, and a backups
folder it could not write to failed every backup as a generic "failed"
(VACUUM INTO's error wraps no permission error), so a NAS user was left
to work out ownership over SSH.

internal/fsaccess words such a folder once: the path, the user the
server runs as, the folder's owner and the fix (PUID/PGID set to the
owner in a container, never PUID=0 for a root-owned folder). It checks by
writing a file of its own, not by reading SQLite's error.

- Start-up: the data folder and its config and database files are
  checked before config.Load and store.Open ("the data folder isn't
  usable: ..."); a backups folder that refuses is a warning.
- Backups: the folder is checked before VACUUM INTO, so a refused one is
  permission_denied, and the notification names the folder and the fix
  (the event carries it as detail). The console's wording names the
  owner problem in every language.
- Scans: a library root the server can reach but not list is unavailable
  with the same words, rather than an empty library scanned OK.

Part of #368.
… fix

The entrypoint ran chown -R on /data with its errors thrown away, so on
a NAS share that refuses chown (Synology ACLs) nothing said why the
server then could not start, and AUDIOSILO_BACKUP_DIR and the audiobooks
mounts were never looked at.

It now also chowns AUDIOSILO_BACKUP_DIR (absolute, never /), then checks
as the user the server will run as that the data and backups folders
can be written and every other mounted folder can be read. For each one
that can't, it prints the folder, the user, the folder's owner, whether
the chown failed, and the fix (PUID/PGID set to the owner, or the
compose user: under --user). It warns and carries on: the server's own
start-up error says the same about the data folder.

scripts/test-entrypoint.sh runs the cases in Docker on the image's
Alpine base; README, docker-compose.yml and CONVENTIONS describe it.

Part of #368.
The data folder check demanded write access to config.yaml, but a
read-only one (mounted so, or edited as root) started before and only a
setting saved from the console needs to write it. It is now checked for
reading only, worded the same way when the server can't read it.

A backups folder the server can't list now names the folder, the user
and the fix in the log behind the console's failed list.

Part of #368.
…nt checks

AUDIOSILO_BACKUP_DIR may be a NAS share other apps keep their backups
in, so the entrypoint now changes the owner of the folder and of
AudioSilo's own backups in it only, not the whole tree.

The read check of each mounted folder, and the stat that names its
owner, now give up after 5 seconds: a hard-mounted share whose server is
gone can block for minutes and must not hold the server's start.

Part of #368.
The hook repeated the model's guards and owned the half of the rule that keeps a deep link from being pulled into the flow later, so only a full-app test could express it. redirectDecision now answers go, stay or none; the hook marks the session on go or stay and writes sessionStorage once, not on every navigation while setup is unfinished.

Part of #365.
fsaccess.cause was a copy of library's pathErrText. One exported fsaccess.Cause serves both, so the scan log and the permission wording cannot drift. Hint builds its 'give user ... access' clause once.

Part of #368.
The notification's detail was taken from the error's text whenever the console code was permission_denied, but only the write check produced the sentence naming the folder, the user and the fix. A chmod or rename refused with EPERM (common on SMB shares) put 'chmod /backups/.audiosilo-...: operation not permitted' in the notification. The backup now wraps any permission refusal once, before the code, the log and the detail.

Part of #368.
The start-up write check of the backups folder only logs a warning, but ran before the listener: with AUDIOSILO_BACKUP_DIR on a hard-mounted share whose server is gone it could hold the start for minutes. It runs in its own goroutine now. The data folder's error prefix is written once.

Part of #368.
The read check listed the whole folder with ls, which stats every entry: on a FUSE or cloud mount with a large flat library root that can pass the 5 s bound and print a false 'can't read' warning. find stops at the first entry. The fix clause is built once and only the mounts kept are unescaped.

Part of #368.
A hand-written config.yaml mounted read-only (or owned by root) with no server_id has one minted at start; the save then failed with a bare 'open /data/config.yaml: permission denied', with none of the folder, user and fix wording the data folder's other failures get. The save's error is worded the same way, and the dataFiles comment says a read-only config starts only when it names the id.

Part of #368.
CheckFolder took every stat error as 'not made yet', so a backups folder behind a parent that refuses the server (EACCES on stat) passed the start-up check and the first backup failed with no earlier warning. Only a missing folder passes now.

Part of #368.
Cause dropped a PathError's path but not a LinkError's, so a backup whose rename was refused read 'the server can't write to /backups: rename /backups/.x.db.tmp /backups/x.db: permission denied' in the log and the notification.

Part of #368.
The entrypoint runs as root before it drops privileges, and several inputs took that further than meant:

- usable_path refused only the literal '/', so --data // (or /., /data/..) ran chown -R on the container's root, mounted libraries included. A '..' component and anything that is the same inode as / are refused, and trailing slashes are trimmed (they also defeated the mount exclusions, so /backups/ was read again as a folder of books).
- The backups chown followed links: in a share other apps write to, a link named audiosilo-x.db had its target re-owned at every start. Regular files only, and chown -h.
- The write check made a file at a predictable name with ': >', which follows a link planted there and empties its target as the server's user. It uses mktemp.
- mkdir, chown, find, the -d test and the write check on the backups folder were unbounded, so AUDIOSILO_BACKUP_DIR on a hard-mounted share whose server is gone held the container's start. Each has the 5 s bound the read check has; the data folder's chown -R stays unbounded (a large folder needs longer).
- BusyBox's timeout leaves a watcher behind each command; one alive at the exec stayed under the server as a zombie for the container's life. They are ended and collected first.
- The owner match compared uid:gid, so a folder the server's user owns with another group got PUID/PGID advice that changes nothing; it compares the uid, as fsaccess.Hint does.

scripts/test-entrypoint.sh has cases for the root spellings, the links, the trailing slash, the owner match and the watchers; the write check's file name and the bounds on the backups folder have none (a hung share is not something the script can mount). CONVENTIONS.md's Folder permissions entry says the rules.

Part of #368.
…roblem

A folder check cut off by the 5 s bound (timeout's 124 or BusyBox's 143)
now says the share did not answer within 5 seconds, with no PUID/PGID
advice, instead of explaining it as a folder owned by another user.
@KodeStar
KodeStar merged commit 60a2ad5 into main Nov 14, 2026
5 checks passed
@KodeStar
KodeStar deleted the fix/new-user-a branch November 14, 2026 00:27
KodeStar added a commit to KodeStar/audiosilo-frontend that referenced this pull request Nov 14, 2026
…#294)

Closes #167. For servers behind a sign-in proxy (Authelia, Authentik,
Cloudflare Access service tokens and similar), a connection can carry
custom HTTP headers that the proxy checks.

## What it does

- **Entry:** a Proxy headers section on the connect screen (type the
address and headers before Continue or a QR scan; pairing keeps them
when the address matches) and a Proxy headers card on the Account page
(pick the address, add, change, remove; a saved value is never shown
again, "Kept as it is"). Strings in all 6 locales.
- **Storage:** secure-store per connection
(`audiosilo.proxy-headers.<id>`, the same place and keychain setting as
the token; on the web, where the token also lives in localStorage).
AsyncStorage holds only a `hasProxyHeaders` marker. Deleted wherever the
token is; the identity migration copies with a read-back check and
deletes the old entry only at clean-up.
- **Scope:** sent with requests to the https address (scheme+host+port)
the set was entered for, which may be the home address; never on a
plain-http request, and not added to requests for other addresses. A 3xx
to another origin is followed by the platform: on Android every stack
sends the headers on to the new origin (drops Authorization); iOS
unchecked (follow-up #295). Reserved names (Authorization, Cookie, Host,
Range, Content-Type, X-AudioSilo-Client, framing headers) and control
characters are refused.
- **Where:** every API request; expo-image, the native players and the
reader's EPUB download via `authHeaders()`; downloads, car and widget
covers via `proxyHeadersFor(url)`. On the web only same-origin fetches
carry them (`<img>`/`<audio>` rely on the proxy's browser sign-in). Not
carried: Chromecast (cast refused, reason `proxy`), the watch apps,
calendar apps. No server change.
- **Sharing on one server:** a new connection (another person, a link
re-pair with no set of its own) keeps the set that carried its
successful sign-in or exchange; a reconnect with a known identity keeps
only its own or the typed set.

## Tests and review

New unit tests for storage, origin scoping, https rule, migration,
reconnect/re-pair, hydrate clean-up, editor and card, and a locale test
that refuses "nowhere else" style claims. Full gate green (559 suites,
7730 tests). Reviewed at the security level (two full /code-review
passes) plus two fix rounds.

## Verified live (web, served player over https)

Headers on 31/31 API requests to the origin after relaunch, none on
covers or the stream (as documented), none to another origin even with
CSP stripped; masked after save and absent from HTML, inputs and
console; reserved names and control characters refused; http-only
connection shows the https caption and no Add; Remove and sign-out clear
storage; sign-in, browse and playback unchanged without headers.

## Verified on Android (Pixel 6a, release build, behind a
header-checking https proxy)

API (264/264 requests), expo-image covers, Media3 playback and downloads
all carried the header and worked; without it the proxy refused and the
app showed an error; an http address shows the https caption. A GET 302
to a second https origin: okhttp fetch, expo-image, downloads and Media3
all followed it and sent the custom header there, dropping
Authorization.

**Still owed (iOS device):** URLSession, SDWebImage covers, downloads,
AVURLAsset, CarPlay cold start, the widget, and the same redirect check.
Android Auto, notification artwork and the reader's EPUB download were
not exercised.

Docs: KodeStar/audiosilo-docs#305. Pairs with
KodeStar/audiosilo-server#381 (same batch, independent).
KodeStar added a commit to KodeStar/audiosilo-docs that referenced this pull request Nov 14, 2026
… for a sign-in proxy (#305)

Docs for v2.7.0 row 15b item 2 and row 15's custom headers.

- **Install on a NAS (closes #277):** a new Synology page (Container
Manager project, the folder's owner as PUID / PGID, Shared Folder
permissions, where the log is; DSM labels not yet checked on a real NAS,
and the page says so). The Docker quickstart creates the data folder
first, gains a `docker run` example, and links Get the apps. Backups
live in `data/backups`; a backups folder elsewhere needs the same owner
and is never a share mounted at `/data/backups`.
- **Troubleshooting `#permission-denied`:** quotes the container's
warning lines, the start error, the "didn't answer within 5 seconds"
line and the library offline line verbatim from the server's verified
output.
- **First run:** only a plain visit to `/admin` opens setup; the setup
step's links open their page (server#365).
- **Proxy headers (fe#167):** "Behind a sign-in proxy" in Remote access,
the Account page's Proxy headers card, https only, what can't send them
(web cross-origin, browser images and audio, Chromecast, watches,
calendar apps). Developer docs: storage key, API client, connect screen,
cast block reason `proxy`.
- **Developer docs:** a Folder permissions section (the server's own
checks and the Docker entrypoint), the backup `detail` field, scanner
offline rule, setup redirect rule.
- Screenshot: `web-player/account.png` recaptured with the Proxy headers
card.

Build green (links and anchors checked). Reviewed with /simplify and
/code-review.

**Merge after** KodeStar/audiosilo-server#381 and
KodeStar/audiosilo-frontend#294; **before** KodeStar/audiosilo-site#69
(its links point here).
KodeStar added a commit to KodeStar/audiosilo-site that referenced this pull request Nov 14, 2026
…roid APK and Obtainium (#69)

From a new user's first install on a Synology NAS (v2.7.0 row 15b item
2).

- Install pages (closes #60): drop "works regardless of ownership";
create the data folder before the first start; Synology owners are
usually 1026 / 100, so set PUID / PGID to the folder's owner; backups
live in `data/backups` and a separate `AUDIOSILO_BACKUP_DIR` needs the
same owner. The shared install snippets start with `mkdir -p data` and
the docker run one gets a PUID / PGID line (1000, the entrypoint's
default).
- Android outside Google Play (closes #27): the Android card (download
page and /player) and a new home page "Get the app" block (App Store and
Google Play badges as equals, plus a hero link) offer the APK from the
latest GitHub release and Obtainium, gated by `ANDROID_LIVE`.
`StoreBadge` owns each store's link and wording; `ApkNote` is shared.

Build and check pass (139 pages, 0 errors). Verified in a browser at 390
and 1440 px: installation, download and home pages, the anchors, no
horizontal overflow, no console errors.

**Merge after the docs PR:** it links docs pages that the docs PR adds
(`/users/getting-started/install-synology`,
`/users/troubleshooting#permission-denied`), and master deploys live.

Pairs with KodeStar/audiosilo-server#381.
KodeStar added a commit that referenced this pull request Nov 14, 2026
…te page, scan-with-the-app wording; LibriVox add keeps an admin's match (#384)

Closes #367, #379, #380. What they leave open moved to #385 (LibriVox
identity edges) and #386 (invites and pairing polish).

## Invites and pairing (#367)
A new user (Synology, Android, coming from Audiobookshelf) scanned the
pairing QR with the phone's camera, landed in the web player, and found
the app still signed out. Our copy said the camera opens the app. It
doesn't: the app claims no domains (and can't on self-hosted addresses),
so only the app's own Scan a QR code pairs it.

- Copy fixed in every console locale and the connect page: "Open the
AudioSilo app and tap Scan a QR code. The phone's camera opens the web
player instead." The invite QR encodes `/connect#code=`, so for invites
the copy says the camera opens the invite page.
- Store links (App Store, Google Play, APK from the latest release) on
`/` and `/connect`, before and after a redeem. `links` gains `ios`,
`android` and a new `apk` (additive).
- The invite page on a phone leads with "Get the app, then come back to
this link", the store links and the web player, and hides the self-scan
QR. The Admin button shows only for an admin's code: the redeem payload
gains `admin: true` (from the code's own user, absent for a listener).
- Invite dialog: 7 days by default (the API's default is unchanged), a
Share button (share sheet, copy fallback on plain http), an optional
password for a new person (the existing `createUser` field), and
warnings for a localhost link, a home-network-only link and https at an
IP address.

## LibriVox add (#379, #380)
- The fold reads the Bridge's new `adopted` result field
(KodeStar/audiosilo-bridge, fix/librivox-fetch-followups). If the folder
was already there, an existing match (an admin's rematch) is kept. If it
is newly placed, the match is replaced and a deleted book's leftover
ASIN/ISBN overrides and enrichment at that path are dropped. If the
field is absent (an older Bridge), behaviour is unchanged.
- The finished event carries `librivox_match` (matched, kept or
unmatched). The notification claims the recording only when matched, and
an unmatched fetch goes out as a warning.
- Fetch params carry `title`, so a failed or parked fetch is named by
its book. Work and recording ids are checked against the Bridge's slug
pattern. A finished job no longer shows its last step. The `not_found`
toast is in the console's words. es/fr/it/pt use the feminine for the
Bridge.
- Not in this PR: rebuilding a book's identity from
`.audiosilo/librivox.json`, which needs scanner work (follow-up).

## Review and verification
- Review: /simplify, then two `/code-review high --fix` passes on the
whole branch. One data-loss finding across the seam (a resumed placement
reported `adopted:false`) was fixed on the Bridge side. Gate green,
including the admin console build and tests, `go test -race`,
golangci-lint and the native-engine run.
- Verified live against a scratch server with the real Bridge on its
branch:
- the invite dialog: 7 days, the password, Share copying on http, all
three warnings;
  - the setup and Pair a device copy;
  - store links on `/` and `/connect`;
- the phone invite page under iPhone 15 and Pixel 7 device profiles,
with the Admin button only for an admin;
- a LibriVox add reporting matched, then Add again keeping an ASIN and
an admin rematch set in between ("kept").
- Not verified live: the unmatched notification (unit-tested). A real
NAS run from #381 is still owed.

Pairs with KodeStar/audiosilo-frontend (feat/new-user-b),
KodeStar/audiosilo-bridge (fix/librivox-fetch-followups) and
KodeStar/audiosilo-docs (docs/new-user-b, to follow).
KodeStar added a commit to KodeStar/audiosilo-site that referenced this pull request Nov 14, 2026
A `draft: true` feature post, `nas-and-sign-in-proxy`, for the work
merged in KodeStar/audiosilo-frontend#294 (proxy headers),
KodeStar/audiosilo-server#381 (setup deep links, Docker permission
failures that name the folder, the user and the fix) and
KodeStar/audiosilo-docs#305 (Install on a Synology NAS, permission
troubleshooting).

Drafts are excluded from the build, so this can merge now; the 2.7.0
release publishes it.

- Claims checked against the code on main and the docs (one review at
high). It quotes the log text and console labels verbatim from the docs,
scopes proxy headers to the one https address they were entered for, and
covers Android passing them on through a redirect.
- Before publishing: the iOS device run for proxy headers (fe#294) and a
real NAS run (server#381) are still owed. The Synology steps were
written for DSM 7.2 and the post says they have not been checked on a
NAS.
- Gate: `yarn build && yarn run check` green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant