Files
Piwigo_Bratonien_Tools/runtime/sync.sh
2026-08-18 16:46:08 +02:00

291 lines
9.7 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
CONFIG_FILE="${PIWIGO_CONFIG:-/etc/bratonien-tools/nc-connector/connection-1.conf}"
[[ -r "$CONFIG_FILE" ]] || { echo "Konfiguration fehlt: $CONFIG_FILE" >&2; exit 1; }
PIWIGO_SYNC_OVERRIDE_VALUE="${PIWIGO_SYNC_OVERRIDE-}"
# shellcheck source=/dev/null
source "$CONFIG_FILE"
NC_ACTIVITY_VIEW="${NC_ACTIVITY_VIEW:-piwigo_showcase_activity}"
NC_DB_VIEW="${NC_DB_VIEW:-piwigo_showcase_sources}"
if [[ -n "$PIWIGO_SYNC_OVERRIDE_VALUE" ]]; then
case "$PIWIGO_SYNC_OVERRIDE_VALUE" in
0|1) PIWIGO_SYNC_ENABLED="$PIWIGO_SYNC_OVERRIDE_VALUE" ;;
*) echo "PIWIGO_SYNC_OVERRIDE muss 0 oder 1 sein." >&2; exit 1 ;;
esac
fi
install -d -m 0750 "$STATE_DIR"
MANIFEST="$STATE_DIR/manifest.tsv"
MAP_FILE="$STATE_DIR/name-map.json"
LOCK_FILE="$STATE_DIR/sync.lock"
ACTIVITY_STATE="$STATE_DIR/activity.json"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
CONNECTION_ID="${CONNECTION_ID:-}"
if [[ -z "$CONNECTION_ID" ]]; then
CONFIG_BASENAME="$(basename -- "$CONFIG_FILE")"
if [[ "$CONFIG_BASENAME" =~ ^connection-([0-9]+)\.conf$ ]]; then
CONNECTION_ID="${BASH_REMATCH[1]}"
else
echo "Connector-ID konnte nicht aus $CONFIG_FILE ermittelt werden." >&2
exit 1
fi
fi
PUBLIC_STATUS_DIR="${PIWIGO_ROOT%/}/_data/bratonien-tools/nc-connector-status"
PUBLIC_STATUS_FILE="$PUBLIC_STATUS_DIR/connection-$CONNECTION_ID.json"
install -d -m 0755 "${PIWIGO_ROOT%/}/_data/bratonien-tools" "$PUBLIC_STATUS_DIR"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
AUTH_MODE=""
API_STATE="not_run"
API_MESSAGE=""
FALLBACK_STATE="not_run"
FALLBACK_MESSAGE=""
ERROR_DETAIL=""
ERROR_STAGE="Vorbereitung"
ERROR_MESSAGE="Synchronisierung fehlgeschlagen"
compact_output() {
tail -n 8 | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]//; s/[[:space:]]$//'
}
write_status() {
local state="$1" message="$2"
local error_detail="$ERROR_DETAIL"
if [[ "$state" != "error" ]]; then
error_detail=""
fi
python3 - "$STATUS_FILE" "$PUBLIC_STATUS_FILE" "$state" "$message" "$AUTH_MODE" "$API_STATE" "$API_MESSAGE" "$FALLBACK_STATE" "$FALLBACK_MESSAGE" "$error_detail" <<'PY'
import json, os, sys, tempfile, time
(path, public_path, state, message, auth_mode, api_state, api_message,
fallback_state, fallback_message, error_detail) = sys.argv[1:]
payload = {
"state": state,
"message": message,
"timestamp": int(time.time()),
"auth_mode": auth_mode,
"api": {"state": api_state, "message": api_message},
"fallback": {"state": fallback_state, "message": fallback_message},
"error_detail": error_detail,
}
def write_json(target):
os.makedirs(os.path.dirname(target), exist_ok=True)
fd, temporary = tempfile.mkstemp(dir=os.path.dirname(target))
with os.fdopen(fd, "w", encoding="utf-8") as handle:
json.dump(payload, handle, ensure_ascii=False)
handle.write("\n")
os.chmod(temporary, 0o644)
os.replace(temporary, target)
write_json(path)
write_json(public_path)
PY
}
failure() {
local exit_code="${1:-1}"
local failed_command="${2:-unbekannt}"
local failed_line="${3:-?}"
trap - ERR
if [[ -z "$ERROR_DETAIL" ]]; then
ERROR_DETAIL="Schritt: $ERROR_STAGE. Exit-Code: $exit_code. Zeile: $failed_line. Fehlgeschlagener Befehl: $failed_command"
fi
write_status error "$ERROR_MESSAGE"
exit "$exit_code"
}
trap 'failure $? "$BASH_COMMAND" "$LINENO"' ERR
run_stage() {
local stage="$1"
local message="$2"
shift 2
local output=""
local exit_code=0
ERROR_STAGE="$stage"
ERROR_MESSAGE="$message"
set +e
output="$("$@" 2>&1)"
exit_code=$?
set -e
if [[ -n "$output" ]]; then
printf '%s\n' "$output"
fi
if [[ "$exit_code" -ne 0 ]]; then
ERROR_DETAIL="Schritt: $stage. Exit-Code: $exit_code."
if [[ -n "$output" ]]; then
ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$output" | compact_output)"
fi
trap - ERR
write_status error "$message"
exit "$exit_code"
fi
}
ERROR_STAGE="Lokalen Zustand prüfen"
ERROR_MESSAGE="Lokaler Connector-Zustand konnte nicht geprüft werden"
NEEDS_LOCAL_REPAIR=0
if [[ ! -s "$MAP_FILE" ]]; then
NEEDS_LOCAL_REPAIR=1
else
set +e
python3 - "$MAP_FILE" "$GALLERY_ROOT" <<'PY'
import json
import sys
from pathlib import Path
mapping = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
gallery = Path(sys.argv[2])
roots = [
target for source, target in mapping.items()
if source.startswith("share:") and "/" not in source
]
raise SystemExit(0 if roots and all((gallery / target).is_dir() for target in roots) else 1)
PY
ROOTS_INTACT=$?
set -e
[[ "$ROOTS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1
fi
if [[ "$NEEDS_LOCAL_REPAIR" == "0" && "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
set +e
php "$SCRIPT_DIR/lib/piwigo-db-check.php" "$MAP_FILE" "$PIWIGO_ROOT"
PIWIGO_ALBUMS_INTACT=$?
set -e
[[ "$PIWIGO_ALBUMS_INTACT" == "0" ]] || NEEDS_LOCAL_REPAIR=1
fi
if [[ "$NEEDS_LOCAL_REPAIR" == "1" ]]; then
GATE_RESULT=0
else
ERROR_STAGE="Nextcloud-Aktivität prüfen"
ERROR_MESSAGE="Nextcloud-Aktivität konnte nicht geprüft werden"
set +e
GATE_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" check \
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW" \
--quiet "$QUIET_SECONDS" --max-wait "$MAX_WAIT_SECONDS" --full-after "$FULL_SYNC_SECONDS" 2>&1)"
GATE_RESULT=$?
set -e
if [[ -n "$GATE_OUTPUT" ]]; then
printf '%s\n' "$GATE_OUTPUT"
fi
fi
if [[ "$GATE_RESULT" == "3" ]]; then
trap - ERR
write_status ok "Keine Änderungen gefunden"
exit 0
fi
if [[ "$GATE_RESULT" != "0" ]]; then
ERROR_DETAIL="Schritt: Nextcloud-Aktivität prüfen. Exit-Code: $GATE_RESULT."
if [[ -n "${GATE_OUTPUT:-}" ]]; then
ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$GATE_OUTPUT" | compact_output)"
fi
trap - ERR
write_status error "Nextcloud-Aktivität konnte nicht geprüft werden"
exit "$GATE_RESULT"
fi
run_stage \
"Nextcloud-Dateiliste lesen" \
"Dateiliste aus Nextcloud konnte nicht erstellt werden" \
python3 "$SCRIPT_DIR/lib/build_manifest.py" \
--host "$NC_DB_HOST" --port "$NC_DB_PORT" --database "$NC_DB_NAME" --user "$NC_DB_USER" \
--password-file "$NC_DB_PASSWORD_FILE" --view "$NC_DB_VIEW" \
--storage-config "$STORAGE_CONFIG" --output "$MANIFEST"
run_stage \
"Lokalen Galeriebaum aktualisieren" \
"Lokaler Galeriebaum konnte nicht aktualisiert werden" \
python3 "$SCRIPT_DIR/lib/shadow_tree.py" \
--manifest "$MANIFEST" --destination "$GALLERY_ROOT" --state "$MAP_FILE"
if [[ "${PIWIGO_SYNC_ENABLED:-0}" == "1" ]]; then
ERROR_STAGE="Piwigo synchronisieren"
ERROR_MESSAGE="Piwigo-Synchronisierung fehlgeschlagen"
set +e
PIWIGO_OUTPUT="$(php "$SCRIPT_DIR/lib/piwigo-sync.php" \
--piwigo-root="$PIWIGO_ROOT" \
--connection-id="$CONNECTION_ID" \
--base-url="http://127.0.0.1" 2>&1)"
PIWIGO_EXIT=$?
set -e
printf '%s\n' "$PIWIGO_OUTPUT"
if grep -q 'Piwigo-Synchronisierung per API erfolgreich' <<<"$PIWIGO_OUTPUT"; then
AUTH_MODE="api"
API_STATE="ok"
API_MESSAGE="API-Synchronisierung erfolgreich"
FALLBACK_STATE="not_needed"
FALLBACK_MESSAGE="Fallback wurde nicht benötigt"
else
API_LINE="$(grep -m1 '^Piwigo-API nicht nutzbar:' <<<"$PIWIGO_OUTPUT" || true)"
if [[ -n "$API_LINE" ]]; then
API_STATE="error"
API_MESSAGE="${API_LINE#Piwigo-API nicht nutzbar: }"
else
API_STATE="error"
API_MESSAGE="API-Synchronisierung war nicht erfolgreich"
fi
if grep -q 'Piwigo-Datenbanksynchronisierung per Benutzername/Passwort-Fallback erfolgreich' <<<"$PIWIGO_OUTPUT"; then
AUTH_MODE="fallback"
FALLBACK_STATE="ok"
FALLBACK_MESSAGE="Benutzername/Passwort-Fallback erfolgreich"
elif [[ "$PIWIGO_EXIT" -ne 0 ]]; then
AUTH_MODE="failed"
FALLBACK_STATE="error"
FALLBACK_MESSAGE="$(tail -n 1 <<<"$PIWIGO_OUTPUT")"
fi
fi
if [[ "$PIWIGO_EXIT" -ne 0 ]]; then
ERROR_DETAIL="Schritt: Piwigo synchronisieren. Exit-Code: $PIWIGO_EXIT. Ausgabe: $(printf '%s\n' "$PIWIGO_OUTPUT" | compact_output)"
trap - ERR
write_status error "Piwigo-Synchronisierung fehlgeschlagen"
exit "$PIWIGO_EXIT"
fi
fi
ERROR_STAGE="Aktivitätsstand speichern"
ERROR_MESSAGE="Aktivitätsstand konnte nicht gespeichert werden"
set +e
COMMIT_OUTPUT="$(python3 "$SCRIPT_DIR/lib/activity_gate.py" commit \
--state "$ACTIVITY_STATE" --host "$NC_DB_HOST" --port "$NC_DB_PORT" \
--database "$NC_DB_NAME" --user "$NC_DB_USER" --password-file "$NC_DB_PASSWORD_FILE" \
--view "$NC_ACTIVITY_VIEW" --source-view "$NC_DB_VIEW" 2>&1)"
COMMIT_EXIT=$?
set -e
if [[ -n "$COMMIT_OUTPUT" ]]; then
printf '%s\n' "$COMMIT_OUTPUT"
fi
if [[ "$COMMIT_EXIT" -ne 0 ]]; then
ERROR_DETAIL="Schritt: Aktivitätsstand speichern. Exit-Code: $COMMIT_EXIT."
if [[ -n "$COMMIT_OUTPUT" ]]; then
ERROR_DETAIL+=" Ausgabe: $(printf '%s\n' "$COMMIT_OUTPUT" | compact_output)"
fi
trap - ERR
write_status error "Aktivitätsstand konnte nicht gespeichert werden"
exit "$COMMIT_EXIT"
fi
trap - ERR
if [[ "$AUTH_MODE" == "fallback" ]]; then
write_status warning "Synchronisierung erfolgreich über Fallback; API war nicht nutzbar"
elif [[ "$AUTH_MODE" == "api" ]]; then
write_status ok "Synchronisierung erfolgreich über API"
else
write_status ok "Synchronisierung erfolgreich"
fi