#!/bin/bash # Compose file, container startup and readiness polling for SyncYomi. # # Sourced by setup_syncyomi.sh; split out to keep it under the 250-line cap. # Sourced rather than run, so it inherits the caller's strict mode and # the helper functions and variables defined above the source line. # --- Step 3: docker-compose stack (host networking, loopback bind) ----------- write_compose() { ensure_dir "${DATA_DIR}/config" # network_mode: host + SyncYomi's host=127.0.0.1 keeps it loopback-only # (reached by the host-networked Caddy) and dodges this host's default-drop # nftables FORWARD chain that would kill a bridge-networked container. umask 077 cat >"${COMPOSE_FILE}" </dev/null 2>&1; then log_ok "Local image ${SYNCYOMI_IMAGE} already built" return 0 fi if [[ ! -d ${SYNCYOMI_SRC}/.git ]]; then log_info "Cloning ${SYNCYOMI_FORK_URL} into ${SYNCYOMI_SRC}" git clone "${SYNCYOMI_FORK_URL}" "${SYNCYOMI_SRC}" fi local version revision version="$(git -C "${SYNCYOMI_SRC}" describe --tags --always)" revision="$(git -C "${SYNCYOMI_SRC}" rev-parse HEAD)" log_info "Building ${SYNCYOMI_IMAGE} from ${SYNCYOMI_SRC} (${version})" # BuildKit is required: the Dockerfile reads $BUILDPLATFORM, which the # legacy builder leaves empty and then rejects as an invalid platform. DOCKER_BUILDKIT=1 docker build \ -t "${SYNCYOMI_IMAGE}" \ --build-arg VERSION="${version}-kuhy" \ --build-arg REVISION="${revision}" \ --build-arg BUILDTIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ "${SYNCYOMI_SRC}" log_ok "Built ${SYNCYOMI_IMAGE} from ${version}" } start_containers() { ensure_local_image log_info "Starting SyncYomi container" docker compose -f "${COMPOSE_FILE}" up -d } # Readiness probe. SyncYomi (v1.1.x) has no public /healthz, but GET # /api/auth/onboard answers 204 (no user yet) or 403 (a user exists) once the # API is serving — either means "up". Echoes the HTTP code; returns 0 if ready. # Usage: syncyomi_ready "http://127.0.0.1:8282" syncyomi_ready() { local base="$1" code code="$(curl -sS -o /dev/null -w '%{http_code}' "${base}/api/auth/onboard" 2>/dev/null || true)" echo "${code}" [[ ${code} == "204" || ${code} == "403" ]] } # --- Step 4: wait until the local API answers -------------------------------- wait_for_syncyomi() { log_info "Waiting for SyncYomi to come up on ${SYNCYOMI_LOCAL}" local i for ((i = 0; i < 60; i++)); do if syncyomi_ready "${SYNCYOMI_LOCAL}" >/dev/null; then log_ok "SyncYomi is up (API answering)" return 0 fi sleep 2 done die "SyncYomi did not become healthy in 120s — check 'docker logs syncyomi'." } # Defensive: SyncYomi generates config/config.toml on first run. Ensure it binds # loopback-only (never 0.0.0.0, which would bypass Caddy/TLS) and serves at root. ensure_local_config() { [[ -f ${CONFIG_TOML} ]] || return 0 local changed=0 if grep -qE '^\s*host\s*=' "${CONFIG_TOML}"; then if ! grep -qE '^\s*host\s*=\s*"127\.0\.0\.1"' "${CONFIG_TOML}"; then sed -i -E 's|^\s*host\s*=.*|host = "127.0.0.1"|' "${CONFIG_TOML}" changed=1 fi fi # Force an empty baseUrl (dedicated subdomain, not a sub-path). if grep -qE '^\s*baseUrl\s*=' "${CONFIG_TOML}" && ! grep -qE '^\s*baseUrl\s*=\s*""' "${CONFIG_TOML}"; then sed -i -E 's|^\s*baseUrl\s*=.*|baseUrl = ""|' "${CONFIG_TOML}" changed=1 fi if [[ ${changed} -eq 1 ]]; then log_info "Patched ${CONFIG_TOML} (host=127.0.0.1, baseUrl empty) — restarting" docker compose -f "${COMPOSE_FILE}" restart wait_for_syncyomi fi } # --- Step 5: create first account + mint API key over the local REST API ----- mint_api_key() { if [[ -f ${TOKEN_FILE} ]]; then log_ok "API key already minted ($(<"${TOKEN_FILE}") stored in ${TOKEN_FILE})" return 0 fi log_info "Creating SyncYomi account and minting API key" local jar body http key jar="$(mktemp)" # shellcheck disable=SC2064 trap "rm -f '${jar}'" RETURN # JSON with proper escaping (password may contain +,/,= from base64). body="$(jq -n --arg u "${SYNCYOMI_USER}" --arg p "${SYNCYOMI_PASSWORD}" \ '{username:$u,password:$p}')" # Onboard the first user only if none exists yet (GET returns 204 when empty). http="$(curl -fsS -o /dev/null -w '%{http_code}' "${SYNCYOMI_LOCAL}/api/auth/onboard" 2>/dev/null || true)" if [[ ${http} == "204" ]]; then http="$(curl -sS -o /dev/null -w '%{http_code}' \ -H 'Content-Type: application/json' -d "${body}" \ "${SYNCYOMI_LOCAL}/api/auth/onboard" 2>/dev/null || true)" [[ ${http} =~ ^2 ]] || die "Onboard failed (HTTP ${http}) — check 'docker logs syncyomi'." log_ok "Created first account '${SYNCYOMI_USER}'" else log_warn "An account already exists (onboard HTTP ${http}); trying stored credentials" fi # Log in to get a session cookie. http="$(curl -sS -c "${jar}" -o /dev/null -w '%{http_code}' \ -H 'Content-Type: application/json' -d "${body}" \ "${SYNCYOMI_LOCAL}/api/auth/login" 2>/dev/null || true)" if [[ ! ${http} =~ ^2 ]]; then log_warn "Login failed (HTTP ${http}). A different account may already own this server." log_warn "Create an API key manually at ${SYNCYOMI_LOCAL} → Settings → API Keys." return 0 fi # Reuse an existing 'tachiyomisy' key if one exists (idempotent); otherwise # create one. scopes must be a non-empty array — SyncYomi rejects a null. key="$(curl -sS -b "${jar}" "${SYNCYOMI_LOCAL}/api/keys" 2>/dev/null | jq -r '[.[] | select(.name=="tachiyomisy")][0].key // empty')" if [[ -n ${key} ]]; then log_ok "Reusing existing 'tachiyomisy' API key" else key="$(curl -sS -b "${jar}" \ -H 'Content-Type: application/json' \ -d '{"name":"tachiyomisy","scopes":["read","write"]}' \ "${SYNCYOMI_LOCAL}/api/keys" 2>/dev/null | jq -r '.key // empty')" [[ -n ${key} ]] || die "API key creation returned no key — check 'docker logs syncyomi'." fi umask 077 printf '%s\n' "${key}" >"${TOKEN_FILE}" chmod 600 "${TOKEN_FILE}" log_ok "Minted API key and stored it in ${TOKEN_FILE} (0600)" }