diff --git a/src/calibre-web/README.md b/src/calibre-web/README.md index c71f0e0..aef995f 100644 --- a/src/calibre-web/README.md +++ b/src/calibre-web/README.md @@ -102,7 +102,9 @@ user table. ### Anonymous browsing (no more double login) This package sets `config_anonbrowse = 1` by default — Calibre-Web's own login screen -is skipped for browser users, who already passed Authentik's SSO gate to get here. +is skipped for browser users, who already passed Authentik's SSO gate to get here. With +the credential stripped at the gate (see above), it is also what makes `/opds` work at +all: Calibre-Web receives an anonymous request and has to be willing to serve one. Anyone wanting their own account (uploads, personal shelves, admin) still logs in via the link in the UI; unauthenticated visitors get the Guest role's permissions instead (configurable under **Admin → User Management → Guest**). @@ -113,6 +115,27 @@ this app), turn anonymous browsing back off first: it also makes Basic auth on ` optional, and without the Traefik credential in front of it, an open `/opds` would be one unauthenticated request away from serving your whole library to anyone. + +### Upgrading from 1.1.x + +The reader model above (anonymous browsing, Kobo sync, the Guest grant) is applied +**once**, by a script the `calibre-web-init` sidecar runs on every deploy and, on a +brand-new install where `app.db` does not exist yet, from LinuxServer's custom-init +hook. A stamp file (`/config/.hola-reader-model-v1`) makes it a no-op from then on, so +anything you change afterwards in **Admin → Edit UI Configuration** or **User +Management** stands. + +Applying it on an upgrade matters because 1.1.x left `/opds` and `/kobo/` exempt with +no gate and Calibre-Web's own Basic auth in front of them, while 1.2.x moves that gate +to Traefik. An install that got the new gate without anonymous browsing would answer +`401` to every reader — the credential is verified and stripped at the edge, and then +Calibre-Web asks for an account the reader cannot supply. + +**Your readers need reconfiguring after this upgrade.** Under 1.1.x they authenticated +with a Calibre-Web account; from 1.2.x they use `https://hola:@/opds` instead. Retrieve the password from the Configuration tab or +`hola config --json`. + ### Kobo sync Kobo sync is **enabled by default** by this package, along with the two settings that diff --git a/src/calibre-web/package.json b/src/calibre-web/package.json index 3f3ca5a..b787104 100644 --- a/src/calibre-web/package.json +++ b/src/calibre-web/package.json @@ -1,6 +1,6 @@ { "name": "calibre-web", - "version": "1.2.1", + "version": "1.2.2", "description": "Calibre-Web — browse, read, and download your Calibre ebook library (Hola app package)", "license": "GPL-3.0", "oci": { diff --git a/src/calibre-web/src/compose.yaml b/src/calibre-web/src/compose.yaml index acecdc1..00de1de 100644 --- a/src/calibre-web/src/compose.yaml +++ b/src/calibre-web/src/compose.yaml @@ -67,72 +67,80 @@ services: echo "[hola] existing Calibre library found at /books — leaving it alone" fi - # Pre-answer Calibre-Web's first-run setup. Its settings live in - # /config/app.db, which doesn't exist until the app container's own s6 - # init creates it — so this can't be done from here. Instead drop a - # script in LinuxServer's custom-init hook: s6 runs it after - # init-calibre-web-config (which creates app.db) and before the app - # starts. Both steps below are guarded so anything changed later in the - # UI is never clobbered: the settings write only fires while the library - # path is still unset, and the Guest grant only while the role is still - # exactly Calibre-Web's untouched default. - cat > /custom-cont-init.d/10-hola-first-run <<'INIT' - #!/usr/bin/with-contenv bash - python3 -c " + # Everything Calibre-Web needs answered in its own settings DB lives in + # ONE script, because it has to run from two places. /config/app.db does + # not exist yet on a fresh install (the app container's s6 init creates + # it), so this sidecar cannot write it — LinuxServer's custom-init hook + # below covers that case. But on an UPGRADE app.db already exists and the + # app container is not recreated at all when only this sidecar changed, + # so the hook never runs and the upgrade silently applies nothing. The + # sidecar therefore runs the same script itself whenever app.db is + # already there. + # + # One-shot, stamped: once /config/.hola-reader-model-v1 exists the script + # is a no-op forever, so whatever the operator later changes in the UI + # stands. Bump the stamp name only for a migration that must run again. + cat > /config/.hola-apply-reader-model <<'APPLY' import sqlite3, os - db = '/config/app.db' - if not os.path.exists(db): + db, stamp = '/config/app.db', '/config/.hola-reader-model-v1' + if not os.path.exists(db) or os.path.exists(stamp): raise SystemExit(0) + # Calibre-Web role bits. + ROLE_DOWNLOAD, ROLE_ANONYMOUS, ROLE_VIEWER = 2, 32, 256 + c = sqlite3.connect(db) - row = c.execute('select config_calibre_dir from settings').fetchone() - if row and row[0]: + + # app.db can exist before Calibre-Web has created its tables (s6 makes the + # file, then populates it). Nothing to do yet — and because the stamp is + # only written on success, the next boot picks it up. + try: + c.execute('select 1 from settings').fetchone() + except sqlite3.OperationalError: raise SystemExit(0) + # The library path is the one value that is genuinely first-run only: + # never point a configured install somewhere else. + row = c.execute('select config_calibre_dir from settings').fetchone() + if not (row and row[0]): + c.execute('update settings set config_calibre_dir = ?', ('/books',)) + + # The reader model itself. Anonymous browsing is what makes the OPDS and + # Kobo paths work at all: Hola gates them at Traefik and STRIPS the + # credential it verified, so Calibre-Web sees an anonymous request and + # must be willing to serve one. Without this an upgraded install answers + # 401 to every reader that just passed the gate. c.execute( - 'update settings set config_calibre_dir = ?, config_kobo_sync = 1,' - ' config_external_port = 443, config_kobo_proxy = 0,' - ' config_anonbrowse = 1', - ('/books',), + 'update settings set config_kobo_sync = 1, config_external_port = 443,' + ' config_kobo_proxy = 0, config_anonbrowse = 1' ) - c.commit() - print('[hola] first-run defaults written to app.db') - " - # Anonymous browsing (above) makes every reader the Guest user, and - # Calibre-Web creates Guest with ROLE_ANONYMOUS (32) and nothing else — - # no ROLE_DOWNLOAD (2), no ROLE_VIEWER (256). Guest can then LIST the - # library over OPDS but every book download answers 401, which is the - # whole point of an OPDS feed. Hola's Traefik credential already gates - # /opds and /kobo/ (and it is stripped before it reaches here, so a - # reader cannot present a Calibre-Web account of its own on that path), - # so the callers that get this far are exactly the ones the operator - # authorized. Grant Guest the two read-only rights that make the app do - # what it says: download a book, and read one in the browser. Runs on - # every boot but only while the role is still the untouched default, so - # an operator who edits Guest in the UI keeps their choice. - python3 -c " - import sqlite3, os - - db = '/config/app.db' - if not os.path.exists(db): - raise SystemExit(0) - - ROLE_DOWNLOAD, ROLE_ANONYMOUS, ROLE_VIEWER = 2, 32, 256 - - c = sqlite3.connect(db) + # Guest is the identity every reader gets. Calibre-Web creates it with + # ROLE_ANONYMOUS and nothing else, so it can list the library but every + # download answers 401 — an OPDS feed you cannot download from. Only + # touched while the role is still that untouched default. row = c.execute('select role from user where name = ?', ('Guest',)).fetchone() - if not row or row[0] != ROLE_ANONYMOUS: - raise SystemExit(0) + if row and row[0] == ROLE_ANONYMOUS: + c.execute( + 'update user set role = ? where name = ?', + (ROLE_ANONYMOUS | ROLE_DOWNLOAD | ROLE_VIEWER, 'Guest'), + ) - c.execute( - 'update user set role = ? where name = ?', - (ROLE_ANONYMOUS | ROLE_DOWNLOAD | ROLE_VIEWER, 'Guest'), - ) c.commit() - print('[hola] granted Guest download + viewer rights') - " + open(stamp, 'w').close() + print('[hola] reader model applied to app.db (anonymous browsing + Guest download)') + APPLY + + # Upgrade path: app.db is already there, so apply now. + python3 /config/.hola-apply-reader-model + + # Fresh-install path: app.db appears later, so run the same script from + # LinuxServer's custom-init hook — s6 runs it after + # init-calibre-web-config creates app.db and before the app starts. + cat > /custom-cont-init.d/10-hola-first-run <<'INIT' + #!/usr/bin/with-contenv bash + python3 /config/.hola-apply-reader-model INIT chmod +x /custom-cont-init.d/10-hola-first-run @@ -161,6 +169,16 @@ services: - ${HOLA_APP_DATA}/books:/books # LinuxServer's custom-init hook, populated by calibre-web-init above. - ${HOLA_APP_DATA}/custom-cont-init.d:/custom-cont-init.d + # Bumped whenever a package change has to reach a RUNNING container. Hola's + # upgrade and restart both run `docker compose up -d`, which recreates only + # the services whose config changed — so a release that only edits the init + # sidecar leaves this container untouched, and everything the sidecar just + # wrote to app.db goes unread until something else happens to restart it. + # Changing a label changes this service's config, so the upgrade recreates it + # and Calibre-Web re-reads its settings. Not in the sh.hola.* namespace: the + # platform owns that one and overwrites it. + labels: + io.try-hola.calibre-web.reader-model: "v1" # Web UI on 8083. Hola routes ingress through Traefik to this container port # (manifest.ingress.port) — no host ports. expose: diff --git a/src/calibre-web/src/manifest.json b/src/calibre-web/src/manifest.json index f9d2c12..c1d419f 100644 --- a/src/calibre-web/src/manifest.json +++ b/src/calibre-web/src/manifest.json @@ -1,6 +1,6 @@ { "name": "calibre-web", - "version": "1.2.1", + "version": "1.2.2", "title": "Calibre-Web", "description": "Browse, read, and download your Calibre ebook library", "icon": "https://raw.githubusercontent.com/try-hola/apps/main/icons/calibre-web.svg",