Files
appfactory-tools/scripts/restore-appfactory.sh
T

364 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
APPFACTORY_ROOT="/opt/appfactory"
BACKUP_FILE_INPUT="${1:-}"
TARGET_USER="${2:-${SUDO_USER:-$USER}}"
log(){ echo "[restore] $*"; }
ok(){ echo "[restore][OK] $*"; }
warn(){ echo "[restore][WARN] $*"; }
fail(){ echo "[restore][FAIL] $*" >&2; exit 1; }
usage() {
cat <<USAGE
Usage:
sudo ./restore-appfactory.sh /path/to/appfactory-backup.tar.gz [target_user]
USAGE
}
env_get_raw() {
local file="$1"
local key="$2"
if [ ! -f "$file" ]; then
return 1
fi
grep -E "^${key}=" "$file" | tail -n 1 | cut -d= -f2- || true
}
env_value_is_non_empty() {
local value="$1"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
[ -n "$value" ]
}
env_has_non_empty_key() {
local file="$1"
local key="$2"
local value
value="$(env_get_raw "$file" "$key" || true)"
env_value_is_non_empty "$value"
}
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
if grep -q "^${key}=" "$file"; then
sed -i "s|^${key}=.*|${key}=${value}|" "$file"
else
echo "${key}=${value}" >> "$file"
fi
}
set_env_if_missing_or_empty() {
local file="$1"
local key="$2"
local value="$3"
if env_has_non_empty_key "$file" "$key"; then
return 0
fi
set_env_value "$file" "$key" "$value"
}
merge_env_missing_keys() {
local source_file="$1"
local target_file="$2"
[ -f "$source_file" ] || return 0
[ -f "$target_file" ] || cp "$source_file" "$target_file"
while IFS= read -r line; do
case "$line" in
""|\#*)
continue
;;
*=*)
key="${line%%=*}"
value="${line#*=}"
if env_has_non_empty_key "$target_file" "$key"; then
continue
fi
set_env_value "$target_file" "$key" "$value"
;;
esac
done < "$source_file"
}
resolve_runtime_uid_gid() {
local user="$1"
local uid
local gid
uid="$(id -u "$user" 2>/dev/null || echo 0)"
gid="$(id -g "$user" 2>/dev/null || echo 0)"
if [ "$uid" = "0" ]; then
uid="1000"
fi
if [ "$gid" = "0" ]; then
gid="1000"
fi
echo "$uid:$gid"
}
[ -n "$BACKUP_FILE_INPUT" ] || { usage; fail "Backup file argument is required"; }
[ "$(id -u)" -eq 0 ] || fail "Run as root via sudo"
[ -f "$BACKUP_FILE_INPUT" ] || fail "Backup file not found: $BACKUP_FILE_INPUT"
command -v realpath >/dev/null 2>&1 || fail "realpath missing"
command -v tar >/dev/null 2>&1 || fail "tar missing"
command -v docker >/dev/null 2>&1 || fail "docker missing"
BACKUP_FILE="$(realpath "$BACKUP_FILE_INPUT")"
if ! id "$TARGET_USER" >/dev/null 2>&1; then
warn "Target user does not exist: $TARGET_USER"
warn "Using numeric fallback 1000:1000 for runtime ownership"
fi
RUNTIME_IDS="$(resolve_runtime_uid_gid "$TARGET_USER")"
APPFACTORY_UID="${RUNTIME_IDS%%:*}"
APPFACTORY_GID="${RUNTIME_IDS##*:}"
[ -S /var/run/docker.sock ] || fail "Docker socket missing: /var/run/docker.sock"
APPFACTORY_DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)"
docker ps >/dev/null 2>&1 || fail "docker ps failed"
log "Backup: $BACKUP_FILE"
log "Target user: $TARGET_USER"
log "Calculated runtime APPFACTORY_UID=$APPFACTORY_UID"
log "Calculated runtime APPFACTORY_GID=$APPFACTORY_GID"
log "Calculated runtime APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_GID"
PRESERVED_ENV=""
EXISTING_ENV="$APPFACTORY_ROOT/config/appfactory.env"
if [ -f "$EXISTING_ENV" ]; then
PRESERVED_ENV="$(mktemp)"
cp "$EXISTING_ENV" "$PRESERVED_ENV"
ok "Preserved existing env before restore: $EXISTING_ENV"
fi
log "Removing existing AppFactory containers if present"
mapfile -t APPFACTORY_CONTAINERS < <(
docker ps -a --format '{{.Names}}' \
| grep -E '^(appfactory-|.*-service$|test-|microsoft-365-service)' \
|| true
)
for container in "${APPFACTORY_CONTAINERS[@]}"; do
[ -n "$container" ] || continue
docker rm -f "$container" >/dev/null 2>&1 || true
ok "Removed container: $container"
done
log "Removing existing AppFactory root"
rm -rf "$APPFACTORY_ROOT"
log "Extracting backup"
cd /
tar -xzf "$BACKUP_FILE"
[ -d "$APPFACTORY_ROOT" ] || fail "Backup did not create $APPFACTORY_ROOT"
log "Ensuring required directories"
mkdir -p \
"$APPFACTORY_ROOT/apps" \
"$APPFACTORY_ROOT/backups" \
"$APPFACTORY_ROOT/config" \
"$APPFACTORY_ROOT/data" \
"$APPFACTORY_ROOT/deploy" \
"$APPFACTORY_ROOT/gateway" \
"$APPFACTORY_ROOT/services" \
"$APPFACTORY_ROOT/templates" \
"$APPFACTORY_ROOT/tools" \
"$APPFACTORY_ROOT/workspace"
ENV_FILE="$APPFACTORY_ROOT/config/appfactory.env"
BACKUP_ENV_COPY=""
if [ -f "$ENV_FILE" ]; then
BACKUP_ENV_COPY="$(mktemp)"
cp "$ENV_FILE" "$BACKUP_ENV_COPY"
fi
if [ -n "$PRESERVED_ENV" ] && [ -f "$PRESERVED_ENV" ]; then
log "Restoring target env and merging missing keys from backup env"
cp "$PRESERVED_ENV" "$ENV_FILE"
if [ -n "$BACKUP_ENV_COPY" ] && [ -f "$BACKUP_ENV_COPY" ]; then
merge_env_missing_keys "$BACKUP_ENV_COPY" "$ENV_FILE"
fi
ENV_SNAPSHOT="$APPFACTORY_ROOT/config/appfactory.env.before-restore-merge-$(date +%Y%m%d-%H%M%S)"
cp "$PRESERVED_ENV" "$ENV_SNAPSHOT"
ok "Saved pre-restore env snapshot: $ENV_SNAPSHOT"
else
[ -f "$ENV_FILE" ] || fail "Missing env file after restore: $ENV_FILE"
log "No existing target env found; using backup env and applying target runtime defaults"
set_env_value "$ENV_FILE" "APPFACTORY_UID" "$APPFACTORY_UID"
set_env_value "$ENV_FILE" "APPFACTORY_GID" "$APPFACTORY_GID"
set_env_value "$ENV_FILE" "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID"
fi
log "Ensuring mandatory env keys without overwriting existing non-empty values"
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_DIR" "$APPFACTORY_ROOT"
set_env_if_missing_or_empty "$ENV_FILE" "WORKSPACE_DIR" "$APPFACTORY_ROOT/workspace"
set_env_if_missing_or_empty "$ENV_FILE" "BACKUP_DIR" "$APPFACTORY_ROOT/backups"
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_UID" "$APPFACTORY_UID"
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_GID" "$APPFACTORY_GID"
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID"
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_ENABLE_HTTPS" "false"
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_DOMAIN" ""
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_PORTAL_DOMAIN" ""
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_GITEA_DOMAIN" ""
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_REGISTRY_DOMAIN" ""
set_env_if_missing_or_empty "$ENV_FILE" "APPFACTORY_PORTAL_PUBLIC_URL" ""
set_env_if_missing_or_empty "$ENV_FILE" "GITEA_DOMAIN" ""
set_env_if_missing_or_empty "$ENV_FILE" "GITEA_ROOT_URL" ""
set_env_if_missing_or_empty "$ENV_FILE" "GITEA_URL" ""
set_env_if_missing_or_empty "$ENV_FILE" "WEBHOOK_URL" ""
set_env_if_missing_or_empty "$ENV_FILE" "GITEA_OAUTH_REDIRECT_URI" ""
set_env_if_missing_or_empty "$ENV_FILE" "GITEA_SSH_PORT" "2222"
set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_OAUTH_ENABLED" "false"
set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_CLIENT_ID" ""
set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_CLIENT_SECRET" ""
set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_REDIRECT_URI" ""
set_env_if_missing_or_empty "$ENV_FILE" "GOOGLE_ALLOWED_DOMAIN" ""
log "Effective runtime env:"
grep -E '^(APPFACTORY_UID|APPFACTORY_GID|APPFACTORY_DOCKER_GID|APPFACTORY_ENABLE_HTTPS|APPFACTORY_DOMAIN|APPFACTORY_PORTAL_DOMAIN|APPFACTORY_GITEA_DOMAIN|APPFACTORY_REGISTRY_DOMAIN|APPFACTORY_PORTAL_PUBLIC_URL|GITEA_DOMAIN|GITEA_ROOT_URL|GITEA_URL|WEBHOOK_URL|GITEA_OAUTH_REDIRECT_URI)=' "$ENV_FILE" || true
DB_FILE="$APPFACTORY_ROOT/data/appfactory/appfactory.db"
[ -f "$DB_FILE" ] || fail "Missing AppFactory DB after restore: $DB_FILE"
COMPOSE_FILE="$APPFACTORY_ROOT/deploy/docker-compose.yml"
[ -f "$COMPOSE_FILE" ] || fail "Missing compose file: $COMPOSE_FILE"
APPFACTORY_UID_EFFECTIVE="$(env_get_raw "$ENV_FILE" "APPFACTORY_UID" || echo "$APPFACTORY_UID")"
APPFACTORY_GID_EFFECTIVE="$(env_get_raw "$ENV_FILE" "APPFACTORY_GID" || echo "$APPFACTORY_GID")"
APPFACTORY_UID_EFFECTIVE="${APPFACTORY_UID_EFFECTIVE%\"}"
APPFACTORY_UID_EFFECTIVE="${APPFACTORY_UID_EFFECTIVE#\"}"
APPFACTORY_GID_EFFECTIVE="${APPFACTORY_GID_EFFECTIVE%\"}"
APPFACTORY_GID_EFFECTIVE="${APPFACTORY_GID_EFFECTIVE#\"}"
if [ "$APPFACTORY_UID_EFFECTIVE" = "0" ]; then
warn "APPFACTORY_UID is 0; changing to 1000 because Gitea cannot run as root"
set_env_value "$ENV_FILE" "APPFACTORY_UID" "1000"
APPFACTORY_UID_EFFECTIVE="1000"
fi
if [ "$APPFACTORY_GID_EFFECTIVE" = "0" ]; then
warn "APPFACTORY_GID is 0; changing to 1000 because Gitea cannot run as root"
set_env_value "$ENV_FILE" "APPFACTORY_GID" "1000"
APPFACTORY_GID_EFFECTIVE="1000"
fi
log "Fixing ownership"
chown -R "$APPFACTORY_UID_EFFECTIVE:$APPFACTORY_GID_EFFECTIVE" "$APPFACTORY_ROOT"
log "Fixing script permissions"
find "$APPFACTORY_ROOT/workspace/appfactory-tools" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true
build_image() {
local image="$1"
local dir="$2"
[ -d "$dir" ] || fail "Missing build directory: $dir"
[ -f "$dir/Dockerfile" ] || fail "Missing Dockerfile: $dir/Dockerfile"
log "Building image: $image from $dir"
docker build -t "$image:latest" "$dir"
}
GENERATE_CADDY="$APPFACTORY_ROOT/workspace/appfactory-tools/scripts/generate-caddyfile.sh"
if [ -f "$GENERATE_CADDY" ]; then
log "Generating Caddyfile before starting Caddy"
"$GENERATE_CADDY" || true
fi
build_image "appfactory-portal" "$APPFACTORY_ROOT/workspace/appfactory-portal"
build_image "appfactory-worker" "$APPFACTORY_ROOT/workspace/appfactory-worker"
build_image "appfactory-monitor" "$APPFACTORY_ROOT/workspace/appfactory-monitor"
build_image "appfactory-webhook" "$APPFACTORY_ROOT/workspace/appfactory-webhook"
log "Deploying core compose stack"
docker compose \
--env-file "$ENV_FILE" \
-f "$COMPOSE_FILE" \
up -d --force-recreate
log "Waiting for core containers"
CORE_CONTAINERS=(
appfactory-caddy
appfactory-gitea
appfactory-registry
appfactory-portal
appfactory-worker
appfactory-monitor
appfactory-webhook
)
deadline=$((SECONDS + 180))
for container in "${CORE_CONTAINERS[@]}"; do
while true; do
if docker inspect "$container" >/dev/null 2>&1; then
state="$(docker inspect -f '{{.State.Status}}' "$container" 2>/dev/null || true)"
health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container" 2>/dev/null || true)"
if [ "$state" = "running" ] && [ "$health" != "unhealthy" ]; then
ok "Container ready: $container health=$health"
break
fi
fi
if [ "$SECONDS" -ge "$deadline" ]; then
docker ps -a
fail "Timeout waiting for container: $container"
fi
sleep 2
done
done
if [ -f "$GENERATE_CADDY" ]; then
log "Regenerating/reloading Caddyfile"
"$GENERATE_CADDY"
fi
PREFLIGHT="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/preflight-check.sh"
[ -f "$PREFLIGHT" ] || fail "Missing preflight script: $PREFLIGHT"
chmod +x "$PREFLIGHT"
log "Running final preflight"
"$PREFLIGHT"
echo
echo "APPFACTORY RESTORE: READY"