Files
libnovel/scripts/pb-init.sh
Admin fe204598a2 fix(pb-init): rewrite ensure_field with pure sh/awk, fix HTTP status parsing
The previous ensure_fields helper used python3 which is not available in
alpine:3.19, causing exit 127 in the cloud init container.

Replaced with ensure_field (per-field, no python/jq) that uses only
busybox sh, wget, sed, and awk. Also fixed the HTTP status grep pattern
in create_collection and ensure_field to match only the response header
line (^  HTTP/) instead of the wget error line, eliminating the spurious
'unexpected status server' warnings.
2026-03-04 14:00:31 +05:00

190 lines
6.7 KiB
Bash
Executable File

#!/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. Helpers ───────────────────────────────────────────────────────────────
create_collection() {
NAME="$1"
BODY="$2"
STATUS=$(wget -qSO- \
--header="Content-Type: application/json" \
--header="Authorization: Bearer $TOKEN" \
--post-data="$BODY" \
"$PB_URL/api/collections" 2>&1 | grep "^ HTTP/" | 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
}
# ensure_field COLLECTION FIELD_NAME FIELD_TYPE
#
# Checks whether FIELD_NAME exists in COLLECTION's schema. If it is missing,
# sends a PATCH with the full current fields list plus the new field appended.
# Uses only busybox sh + wget + sed/awk — no python/jq required.
ensure_field() {
COLL="$1"
FIELD_NAME="$2"
FIELD_TYPE="$3"
SCHEMA=$(wget -qO- \
--header="Authorization: Bearer $TOKEN" \
"$PB_URL/api/collections/$COLL" 2>/dev/null)
# Check if the field already exists (look for "name":"<FIELD_NAME>" in the fields array)
if echo "$SCHEMA" | grep -q "\"name\":\"$FIELD_NAME\""; then
log "field $COLL.$FIELD_NAME already exists — skipping"
return
fi
COLLECTION_ID=$(echo "$SCHEMA" | sed 's/.*"id":"\([^"]*\)".*/\1/')
if [ -z "$COLLECTION_ID" ] || [ "$COLLECTION_ID" = "$SCHEMA" ]; then
log "WARNING: could not get id for collection $COLL — skipping ensure_field"
return
fi
# Extract current fields array (everything between the outermost [ ] of "fields":[...])
# and append the new field object before the closing bracket.
CURRENT_FIELDS=$(echo "$SCHEMA" | sed 's/.*"fields":\(\[.*\]\).*/\1/')
# Strip the trailing ] and append the new field
TRIMMED=$(echo "$CURRENT_FIELDS" | sed 's/]$//')
NEW_FIELDS="${TRIMMED},{\"name\":\"${FIELD_NAME}\",\"type\":\"${FIELD_TYPE}\"}]"
PATCH_BODY="{\"fields\":${NEW_FIELDS}}"
STATUS=$(wget -qSO- \
--header="Content-Type: application/json" \
--header="Authorization: Bearer $TOKEN" \
--body-data="$PATCH_BODY" \
--method=PATCH \
"$PB_URL/api/collections/$COLLECTION_ID" 2>&1 | grep "^ HTTP/" | awk '{print $2}')
case "$STATUS" in
200|201) log "patched $COLL — added field: $FIELD_NAME ($FIELD_TYPE)" ;;
*) log "WARNING: patch returned $STATUS when adding $FIELD_NAME to $COLL" ;;
esac
}
# ─── 4. Create collections (idempotent — skips if already exist) ─────────────
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": "user_id", "type": "text"},
{"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"}
]
}'
# ─── 5. Schema migrations (idempotent field additions) ───────────────────────
# Ensures fields added after initial deploy are present in existing instances.
ensure_field "progress" "user_id" "text"
log "all collections ready"