Initial Commit
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user