Files
Inventarsystem/manage-version.sh
T
2026-04-10 14:48:52 +02:00

338 lines
9.7 KiB
Bash

#!/bin/bash
# Standalone version manager for this repo
# Supports pinning to a commit/tag/branch, one-time use, clearing the lock,
# applying the lock, listing refs, and showing status. Optional restart.
set -euo pipefail
PROJECT_DIR="$(dirname "$(readlink -f "$0")")"
cd "$PROJECT_DIR"
LOCK_FILE="$PROJECT_DIR/.version-lock"
RESTART=false
FORCE=false
BACKUP_BASE_DIR="/var/backups"
LOG_DIR="$PROJECT_DIR/logs"
VM_LOG="$LOG_DIR/version_manager.log"
# Directories/files to preserve across version switches
# Data paths we want to restore from backup after a version switch
DATA_PATHS=(
"certs"
"Images"
"logs"
"Web/uploads"
"Web/thumbnails"
"Web/QRCodes"
"Web/previews"
)
# Ensure log directory exists
mkdir -p "$LOG_DIR" >/dev/null 2>&1 || true
chmod 777 "$LOG_DIR" >/dev/null 2>&1 || true
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$VM_LOG"; }
err() { echo "ERROR: $*" | tee -a "$VM_LOG" >&2; }
usage() {
cat <<USAGE
Usage:
$0 pin <ref> [--restart] [--force] Persistently lock to a commit/tag/branch and deploy it
$0 use <ref> [--restart] [--force] Use ref once (no persistence) and deploy it
$0 clear [--restart] [--force] Remove lock, switch back to main, pull latest
$0 apply [--restart] [--force] Re-apply the current lock (if any)
$0 status Show current commit and lock state
$0 list [--tags|--commits] List tags or recent commits (default: tags)
Notes:
- <ref> can be a tag name, branch name, or full/short commit hash
- --restart calls ./restart.sh after switching
- --force discards local changes (git reset --hard) if needed
USAGE
}
ensure_git_ready() {
# Avoid "dubious ownership" errors
git config --global --add safe.directory "$PROJECT_DIR" >/dev/null 2>&1 || true
# Fetch refs and tags
git fetch --all --tags --prune
}
ensure_clean_or_force() {
if ! git diff --quiet || ! git diff --cached --quiet; then
if [ "$FORCE" = true ]; then
log "Discarding local changes (force)"
git reset --hard
git clean -fdx >/dev/null 2>&1 || true
else
err "Working tree has local changes. Re-run with --force to discard."
exit 2
fi
fi
}
restart_if_requested() {
if [ "$RESTART" = true ]; then
if [ -x "$PROJECT_DIR/restart.sh" ]; then
log "Restarting services..."
"$PROJECT_DIR/restart.sh"
else
log "restart.sh not found or not executable; skipping restart"
fi
fi
}
# Create a full backup before switching versions (files + DB CSVs)
LAST_BACKUP_ARCHIVE=""
LAST_BACKUP_DIRNAME=""
create_pre_switch_backup() {
local ts backup_name backup_dir backup_archive
ts=$(date +"%Y-%m-%d_%H-%M-%S")
backup_name="Inventarsystem-pre-switch-$ts"
backup_dir="$BACKUP_BASE_DIR/$backup_name"
backup_archive="$BACKUP_BASE_DIR/$backup_name.tar.gz"
log "Creating pre-switch backup at $backup_archive"
sudo mkdir -p "$backup_dir" || { err "Failed to create backup directory"; return 1; }
# Copy project files (exclude .venv to reduce size, keep .git for reference)
log "Copying project files to backup directory..."
sudo rsync -a --delete \
--exclude='.venv' \
--exclude='__pycache__' \
--exclude='*.pyc' \
"$PROJECT_DIR/" "$backup_dir/" || log "Warning: rsync file copy encountered issues"
# Database CSV backup using existing helper
log "Backing up MongoDB to CSVs..."
sudo mkdir -p "$backup_dir/mongodb_backup" || true
if [ -x "$PROJECT_DIR/run-backup.sh" ]; then
if sudo -E "$PROJECT_DIR/run-backup.sh" --db Inventarsystem --uri mongodb://localhost:27017/ --out "$backup_dir/mongodb_backup" >> "$VM_LOG" 2>&1; then
log "Database backup completed"
else
err "Database backup failed; continuing with file backup only"
fi
else
log "run-backup.sh not found; skipping DB backup"
fi
# Compress backup
log "Compressing backup archive..."
if sudo tar -czf "$backup_archive" -C "$BACKUP_BASE_DIR" "$backup_name"; then
log "Backup archived to $backup_archive"
sudo rm -rf "$backup_dir" || true
# Set readable permissions
sudo chmod 644 "$backup_archive" || true
# Export for subsequent restore step
LAST_BACKUP_ARCHIVE="$backup_archive"
LAST_BACKUP_DIRNAME="$backup_name"
else
err "Failed to create compressed backup archive"
return 1
fi
}
# Restore key data from the backup archive into current working tree
restore_from_backup_archive() {
if [ -z "$LAST_BACKUP_ARCHIVE" ] || [ ! -f "$LAST_BACKUP_ARCHIVE" ]; then
log "No backup archive available to restore data from; skipping data restore"
return 0
fi
local tmpdir
tmpdir=$(mktemp -d /tmp/inventarsystem_restore_XXXXXX)
log "Extracting backup archive $LAST_BACKUP_ARCHIVE to $tmpdir"
if ! sudo tar -xzf "$LAST_BACKUP_ARCHIVE" -C "$tmpdir"; then
err "Failed to extract backup archive; skipping data restore"
rm -rf "$tmpdir" || true
return 1
fi
local src_root="$tmpdir/$LAST_BACKUP_DIRNAME"
# Some backups might extract without top-level dir; fallback
if [ ! -d "$src_root" ]; then
src_root="$tmpdir"
fi
log "Restoring data directories from backup..."
for rel in "${DATA_PATHS[@]}"; do
if [ -e "$src_root/$rel" ]; then
mkdir -p "$PROJECT_DIR/$(dirname "$rel")" 2>/dev/null || true
rsync -a "$src_root/$rel" "$PROJECT_DIR/$(dirname "$rel")/" >> "$VM_LOG" 2>&1 || log "Warning: failed to restore $rel"
log "Restored: $rel"
else
log "Not in backup (skipped): $rel"
fi
done
rm -rf "$tmpdir" || true
}
# Ensure permissions on critical data directories
ensure_permissions() {
# Logs should be writable
mkdir -p "$PROJECT_DIR/logs" && chmod 777 "$PROJECT_DIR/logs" 2>/dev/null || true
# Web data directories readable/executable by server
for d in "$PROJECT_DIR/Web/uploads" "$PROJECT_DIR/Web/thumbnails" "$PROJECT_DIR/Web/QRCodes" "$PROJECT_DIR/Web/previews" "$PROJECT_DIR/Images"; do
[ -d "$d" ] && chmod -R 755 "$d" 2>/dev/null || true
done
# SSL cert perms (key must be 600)
if [ -f "$PROJECT_DIR/certs/inventarsystem.key" ]; then
chmod 600 "$PROJECT_DIR/certs/inventarsystem.key" 2>/dev/null || true
fi
}
checkout_ref() {
local ref="$1"
# Try tag
if git show-ref --verify --quiet "refs/tags/$ref"; then
git checkout -B locked-tag-"$ref" "tags/$ref"
return 0
fi
# Try remote branch
if git show-ref --verify --quiet "refs/remotes/origin/$ref"; then
git checkout -B locked-branch-"$ref" "origin/$ref"
return 0
fi
# Try local branch
if git show-ref --verify --quiet "refs/heads/$ref"; then
git checkout "$ref"
return 0
fi
# Try commit-ish
if git rev-parse --verify --quiet "$ref^{commit}" >/dev/null; then
git checkout -B locked-commit "$ref"
return 0
fi
err "Could not resolve ref '$ref' to tag/branch/commit"
exit 3
}
deploy_main_latest() {
ensure_clean_or_force
if git show-ref --verify --quiet refs/heads/main; then
git checkout main
else
git checkout -B main origin/main
fi
git pull --ff-only
}
cmd_status() {
local head_commit
head_commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
echo "Current commit: $head_commit"
if [ -f "$LOCK_FILE" ]; then
echo "Version lock: $(cat "$LOCK_FILE")"
else
echo "Version lock: none (tracking main)"
fi
}
cmd_list() {
local mode="tags"
for a in "$@"; do
case "$a" in
--tags) mode="tags" ;;
--commits) mode="commits" ;;
esac
done
ensure_git_ready
if [ "$mode" = "tags" ]; then
git tag --list | sort -V
else
git --no-pager log --oneline -n 30
fi
}
cmd_pin() {
local ref="$1"
ensure_git_ready
# Always produce a backup before switching
create_pre_switch_backup || log "Backup step had issues; proceeding with caution"
ensure_clean_or_force
echo "$ref" > "$LOCK_FILE"
checkout_ref "$ref"
# Restore data from the backup archive
restore_from_backup_archive
ensure_permissions
log "Pinned to: $ref (commit $(git rev-parse --short HEAD))"
restart_if_requested
}
cmd_use() {
local ref="$1"
ensure_git_ready
create_pre_switch_backup || log "Backup step had issues; proceeding with caution"
ensure_clean_or_force
checkout_ref "$ref"
restore_from_backup_archive
ensure_permissions
log "Using (one-time): $ref (commit $(git rev-parse --short HEAD))"
restart_if_requested
}
cmd_clear() {
ensure_git_ready
create_pre_switch_backup || log "Backup step had issues; proceeding with caution"
[ -f "$LOCK_FILE" ] && rm -f "$LOCK_FILE"
deploy_main_latest
restore_from_backup_archive
ensure_permissions
log "Cleared version lock; now on main @ $(git rev-parse --short HEAD)"
restart_if_requested
}
cmd_apply() {
ensure_git_ready
create_pre_switch_backup || log "Backup step had issues; proceeding with caution"
if [ -f "$LOCK_FILE" ]; then
ensure_clean_or_force
local ref
ref=$(cat "$LOCK_FILE")
checkout_ref "$ref"
log "Applied lock: $ref (commit $(git rev-parse --short HEAD))"
else
log "No lock present; keeping main"
deploy_main_latest
fi
restore_from_backup_archive
ensure_permissions
restart_if_requested
}
# Parse global flags that can appear after subcommand too
parse_tail_flags() {
for a in "$@"; do
case "$a" in
--restart) RESTART=true ;;
--force) FORCE=true ;;
esac
done
}
main() {
local cmd="${1:-}"; shift || true
case "$cmd" in
pin)
[ $# -ge 1 ] || { usage; exit 1; }
local ref="$1"; shift; parse_tail_flags "$@"; cmd_pin "$ref" ;;
use)
[ $# -ge 1 ] || { usage; exit 1; }
local ref="$1"; shift; parse_tail_flags "$@"; cmd_use "$ref" ;;
clear)
parse_tail_flags "$@"; cmd_clear ;;
apply)
parse_tail_flags "$@"; cmd_apply ;;
status)
cmd_status ;;
list)
cmd_list "$@" ;;
--help|-h|help|"")
usage ;;
*)
err "Unknown command: $cmd"; usage; exit 1 ;;
esac
}
main "$@"