119 lines
4.9 KiB
Bash
Executable File
119 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# export-backup.sh – Standalone-Export für CalBook (braucht nur Docker + .env)
|
||
# Funktioniert mit jeder CalBook-Version, auch ohne die Backup-API.
|
||
# Output: calbook-backup-YYYY-MM-DD.json
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
ENV_FILE="${ROOT_DIR}/.env"
|
||
|
||
if [[ ! -f "${ENV_FILE}" ]]; then
|
||
echo "Fehler: .env nicht gefunden in ${ROOT_DIR}"
|
||
exit 1
|
||
fi
|
||
|
||
get_env() {
|
||
local key="$1"
|
||
local line val
|
||
line="$(grep -E "^${key}=" "${ENV_FILE}" | head -n1 || true)"
|
||
[[ -z "${line}" ]] && { echo ""; return; }
|
||
val="${line#*=}"
|
||
val="${val#\"}"; val="${val%\"}"
|
||
val="${val#\'}"; val="${val%\'}"
|
||
echo "${val}"
|
||
}
|
||
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
echo "Fehler: Docker wird benötigt."
|
||
exit 1
|
||
fi
|
||
|
||
STACK_NAME="$(get_env STACK_NAME)"
|
||
[[ -z "${STACK_NAME}" ]] && STACK_NAME="calbook"
|
||
|
||
CONTAINER="${STACK_NAME}-db"
|
||
|
||
if ! docker ps --format '{{.Names}}' | grep -qx "${CONTAINER}"; then
|
||
echo "Fehler: DB-Container '${CONTAINER}' läuft nicht."
|
||
exit 1
|
||
fi
|
||
|
||
OUTPUT_FILE="calbook-backup-$(date '+%Y-%m-%d').json"
|
||
TEMP_DIR="$(mktemp -d)"
|
||
trap 'rm -rf "${TEMP_DIR}"' EXIT
|
||
|
||
echo "Exportiere Datenbank via Container '${CONTAINER}'..."
|
||
|
||
run_query() {
|
||
docker exec "${CONTAINER}" psql -U calbook -d calbook -t -A -F $'\t' -c "$1" > "$2"
|
||
}
|
||
|
||
# ── Settings ─────────────────────────────────────────
|
||
run_query "SELECT json_agg(t) FROM (SELECT key, value FROM \"Setting\" WHERE key NOT IN ('PUBLIC_URL', 'NEXTAUTH_URL', 'APP_BASE_URL') ORDER BY key) t;" \
|
||
"${TEMP_DIR}/settings.json"
|
||
|
||
# ── Users ────────────────────────────────────────────
|
||
run_query "SELECT json_agg(t) FROM (SELECT id, name, email, \"hashedPassword\", role, slug, bio, \"avatarUrl\", timezone, \"isActive\", \"createdAt\", \"updatedAt\" FROM \"User\" ORDER BY email) t;" \
|
||
"${TEMP_DIR}/users.json"
|
||
|
||
# ── Calendar connections ─────────────────────────────
|
||
run_query "SELECT json_agg(t) FROM (SELECT id, \"userId\", name, \"bookingAllowedWeekdays\", \"bookingDayStartTime\", \"bookingDayEndTime\", \"bookingDayRangesJson\", url, username, \"notificationEmail\", \"encryptedPassword\", color, \"syncEnabled\", \"lastSyncedAt\", \"createdAt\", \"updatedAt\" FROM \"CalendarConn\" ORDER BY \"createdAt\") t;" \
|
||
"${TEMP_DIR}/calendars.json"
|
||
|
||
# ── Appointments ─────────────────────────────────────
|
||
run_query "SELECT json_agg(t) FROM (SELECT * FROM \"Appointment\" ORDER BY \"startAt\") t;" \
|
||
"${TEMP_DIR}/appointments.json"
|
||
|
||
# ── Busy blocks ──────────────────────────────────────
|
||
run_query "SELECT json_agg(t) FROM (SELECT * FROM \"BusyBlock\" ORDER BY \"startAt\") t;" \
|
||
"${TEMP_DIR}/busyblocks.json"
|
||
|
||
# ── Delivery issues ──────────────────────────────────
|
||
run_query "SELECT json_agg(t) FROM (SELECT * FROM \"DeliveryIssue\" ORDER BY \"firstSeenAt\") t;" \
|
||
"${TEMP_DIR}/delivery.json"
|
||
|
||
# ── Sync runs + logs ─────────────────────────────────
|
||
run_query "SELECT json_agg(t) FROM (SELECT r.*, (SELECT json_agg(e) FROM (SELECT * FROM \"CalendarSyncLogEntry\" e WHERE e.\"syncRunId\" = r.id ORDER BY e.\"createdAt\") e) AS entries FROM \"CalendarSyncRun\" r ORDER BY r.\"startedAt\") t;" \
|
||
"${TEMP_DIR}/syncruns.json"
|
||
|
||
# ── Assemble via node (keine externen Dependencies) ──
|
||
echo "Erstelle Backup-Datei..."
|
||
|
||
EXPORTED_AT="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
|
||
CALDAV_KEY="$(get_env CALDAV_ENCRYPTION_KEY)"
|
||
|
||
node - "$CALDAV_KEY" "$EXPORTED_AT" "$OUTPUT_FILE" "$TEMP_DIR" << 'NODEEOF'
|
||
const fs = require('fs');
|
||
const caldavKey = process.argv[2] || "";
|
||
const exportedAt = process.argv[3];
|
||
const outputFile = process.argv[4];
|
||
const tempDir = process.argv[5];
|
||
|
||
const files = ['settings','users','calendars','appointments','busyblocks','delivery','syncruns'];
|
||
const data = {};
|
||
for (const f of files) {
|
||
const raw = fs.readFileSync(tempDir + '/' + f + '.json', 'utf8').trim();
|
||
data[f] = raw ? JSON.parse(raw) : [];
|
||
}
|
||
const backup = {
|
||
version: 1,
|
||
exportedAt: exportedAt,
|
||
caldavEncryptionKey: caldavKey || undefined,
|
||
settings: data.settings || [],
|
||
users: data.users || [],
|
||
calendarConns: data.calendars || [],
|
||
appointments: data.appointments || [],
|
||
busyBlocks: data.busyblocks || [],
|
||
deliveryIssues: data.delivery || [],
|
||
syncRuns: data.syncruns || []
|
||
};
|
||
fs.writeFileSync(outputFile, JSON.stringify(backup, null, 2));
|
||
NODEEOF
|
||
|
||
echo
|
||
echo "Backup erstellt: ${OUTPUT_FILE}"
|
||
echo "Größe: $(du -h "${OUTPUT_FILE}" | cut -f1)"
|
||
echo
|
||
echo "Import auf neuer Instanz:"
|
||
echo " Admin → Backup → Datei auswählen → Importieren"
|