From a0344b36d72f791995909a59df28fe27c42a4e72 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 3 Mar 2026 20:09:25 +0500 Subject: [PATCH] feat(infra): add pb-init sidecar to create PocketBase collections on startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts/pb-init.sh — an idempotent alpine sh script that authenticates with the PocketBase admin API and POSTs all required collection schemas (books, chapters_idx, ranking, progress, audio_cache, app_users) before any application service starts. 400/422 responses are treated as success so it is safe to run on every docker compose up. Wire pb-init into docker-compose.yml: - pb-init service: alpine:3.19, depends on pocketbase healthy, mounts script - scraper and ui both depend on pb-init via service_completed_successfully, guaranteeing collections exist before the first request hits app_users --- docker-compose.yml | 20 +++++++ scripts/pb-init.sh | 136 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100755 scripts/pb-init.sh diff --git a/docker-compose.yml b/docker-compose.yml index e07691f..e466290 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,6 +59,22 @@ services: timeout: 5s retries: 5 + # ─── PocketBase collection bootstrap ──────────────────────────────────────── + # One-shot init container: creates all required collections via the admin API + # and exits. Idempotent — safe to run on every `docker compose up`. + pb-init: + image: alpine:3.19 + depends_on: + pocketbase: + condition: service_healthy + environment: + POCKETBASE_URL: "http://pocketbase:8090" + POCKETBASE_ADMIN_EMAIL: "${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}" + POCKETBASE_ADMIN_PASSWORD: "${POCKETBASE_ADMIN_PASSWORD:-changeme123}" + volumes: + - ./scripts/pb-init.sh:/pb-init.sh:ro + entrypoint: ["sh", "/pb-init.sh"] + # ─── Browserless ──────────────────────────────────────────────────────────── browserless: image: ghcr.io/browserless/chromium:latest @@ -94,6 +110,8 @@ services: #container_name: libnovel-scraper restart: unless-stopped depends_on: + pb-init: + condition: service_completed_successfully pocketbase: condition: service_healthy minio: @@ -142,6 +160,8 @@ services: container_name: libnovel-ui restart: unless-stopped depends_on: + pb-init: + condition: service_completed_successfully scraper: condition: service_healthy pocketbase: diff --git a/scripts/pb-init.sh b/scripts/pb-init.sh new file mode 100755 index 0000000..cfbfd32 --- /dev/null +++ b/scripts/pb-init.sh @@ -0,0 +1,136 @@ +#!/bin/sh +# pb-init.sh — idempotent PocketBase collection bootstrap +# +# Creates all collections required by libnovel. Safe to re-run: POST returns +# 400/422 when a collection already exists; both are treated as success. +# +# Required env vars (with defaults): +# POCKETBASE_URL http://pocketbase:8090 +# POCKETBASE_ADMIN_EMAIL admin@libnovel.local +# POCKETBASE_ADMIN_PASSWORD changeme123 + +set -e + +PB_URL="${POCKETBASE_URL:-http://pocketbase:8090}" +PB_EMAIL="${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}" +PB_PASSWORD="${POCKETBASE_ADMIN_PASSWORD:-changeme123}" + +log() { echo "[pb-init] $*"; } + +# ─── 1. Wait for PocketBase to be ready ────────────────────────────────────── +log "waiting for PocketBase at $PB_URL ..." +until wget -qO- "$PB_URL/api/health" > /dev/null 2>&1; do + sleep 2 +done +log "PocketBase is up" + +# ─── 2. Authenticate and obtain a superuser token ──────────────────────────── +log "authenticating as $PB_EMAIL ..." +AUTH_RESPONSE=$(wget -qO- \ + --header="Content-Type: application/json" \ + --post-data="{\"identity\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\"}" \ + "$PB_URL/api/collections/_superusers/auth-with-password") + +TOKEN=$(echo "$AUTH_RESPONSE" | sed 's/.*"token":"\([^"]*\)".*/\1/') +if [ -z "$TOKEN" ] || [ "$TOKEN" = "$AUTH_RESPONSE" ]; then + log "ERROR: failed to obtain auth token. Response: $AUTH_RESPONSE" + exit 1 +fi +log "auth token obtained" + +# ─── 3. Helper: create a collection (ignores 400/422 already-exists) ───────── +create_collection() { + NAME="$1" + BODY="$2" + STATUS=$(wget -qSO- \ + --header="Content-Type: application/json" \ + --header="Authorization: $TOKEN" \ + --post-data="$BODY" \ + "$PB_URL/api/collections" 2>&1 | grep "HTTP/" | tail -1 | awk '{print $2}') + case "$STATUS" in + 200|201) log "created collection: $NAME" ;; + 400|422) log "collection already exists (skipped): $NAME" ;; + *) log "WARNING: unexpected status $STATUS for collection: $NAME" ;; + esac +} + +# ─── 4. Create collections ──────────────────────────────────────────────────── + +create_collection "books" '{ + "name": "books", + "type": "base", + "fields": [ + {"name": "slug", "type": "text", "required": true}, + {"name": "title", "type": "text", "required": true}, + {"name": "author", "type": "text"}, + {"name": "cover", "type": "text"}, + {"name": "status", "type": "text"}, + {"name": "genres", "type": "json"}, + {"name": "summary", "type": "text"}, + {"name": "total_chapters", "type": "number"}, + {"name": "source_url", "type": "text"}, + {"name": "ranking", "type": "number"}, + {"name": "meta_updated", "type": "date"} + ] +}' + +create_collection "chapters_idx" '{ + "name": "chapters_idx", + "type": "base", + "fields": [ + {"name": "slug", "type": "text", "required": true}, + {"name": "number", "type": "number", "required": true}, + {"name": "title", "type": "text"}, + {"name": "date_label", "type": "text"} + ] +}' + +create_collection "ranking" '{ + "name": "ranking", + "type": "base", + "fields": [ + {"name": "rank", "type": "number", "required": true}, + {"name": "slug", "type": "text", "required": true}, + {"name": "title", "type": "text"}, + {"name": "author", "type": "text"}, + {"name": "cover", "type": "text"}, + {"name": "status", "type": "text"}, + {"name": "genres", "type": "json"}, + {"name": "source_url", "type": "text"}, + {"name": "updated", "type": "date"} + ] +}' + +create_collection "progress" '{ + "name": "progress", + "type": "base", + "fields": [ + {"name": "session_id", "type": "text", "required": true}, + {"name": "slug", "type": "text", "required": true}, + {"name": "chapter", "type": "number"}, + {"name": "updated", "type": "date"} + ] +}' + +create_collection "audio_cache" '{ + "name": "audio_cache", + "type": "base", + "fields": [ + {"name": "cache_key", "type": "text", "required": true}, + {"name": "filename", "type": "text"}, + {"name": "updated", "type": "date"} + ] +}' + +create_collection "app_users" '{ + "name": "app_users", + "type": "base", + "fields": [ + {"name": "username", "type": "text", "required": true}, + {"name": "password_hash", "type": "text", "required": true}, + {"name": "role", "type": "text"}, + {"name": "created", "type": "date"} + ] +}' + +log "all collections ready"