diff --git a/.docker-build.env b/.docker-build.env deleted file mode 100644 index 58471c1..0000000 --- a/.docker-build.env +++ /dev/null @@ -1,11 +0,0 @@ -NUITKA_BUILD=0 -INVENTAR_HTTP_PORT=10000 -INVENTAR_HTTP_PORTS=10000 -INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42 -INVENTAR_APP_CPUS=1.0 -INVENTAR_APP_MEM_LIMIT=384m -INVENTAR_APP_MEM_SWAP_LIMIT=768m -INVENTAR_WORKERS=4 -INVENTAR_THREADS=2 -INVENTAR_WORKER_TIMEOUT=30 -INVENTAR_WORKER_CONNECTIONS=100 \ No newline at end of file diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml index f1c2a26..6dc0c90 100644 --- a/.github/workflows/release-docker.yml +++ b/.github/workflows/release-docker.yml @@ -256,7 +256,7 @@ jobs: EOF # Copy runtime scripts and config if present - for f in start.sh stop.sh restart.sh backup.sh config.json update.sh; do + for f in start.sh stop.sh restart.sh backup.sh restore.sh config.json update.sh; do if [ -f "$f" ]; then cp "$f" "release-bundle/$(basename "$f")" fi diff --git a/backup.sh b/backup.sh index 7609b62..62d58ba 100755 --- a/backup.sh +++ b/backup.sh @@ -29,31 +29,12 @@ EOF parse_args() { while [[ $# -gt 0 ]]; do case "$1" in - --invoice-archive-dir) - INVOICE_ARCHIVE_DIR="$2" - shift 2 - ;; - --invoice-keep-days) - KEEP_DAYS="$2" - shift 2 - ;; - --mode) - MODE="$2" - shift 2 - ;; - --base-name) - BASE_NAME="$2" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Error: unknown option '$1'" >&2 - usage - exit 2 - ;; + --invoice-archive-dir) INVOICE_ARCHIVE_DIR="$2"; shift 2 ;; + --invoice-keep-days) KEEP_DAYS="$2"; shift 2 ;; + --mode) MODE="$2"; shift 2 ;; + --base-name) BASE_NAME="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Error: unknown option '$1'" >&2; usage; exit 2 ;; esac done } @@ -67,70 +48,50 @@ cleanup_old_archives() { if [[ -n "$KEEP_DAYS" ]]; then find "$INVOICE_ARCHIVE_DIR" -maxdepth 1 -type f -mtime +"$KEEP_DAYS" -print -delete 2>/dev/null || true fi - find "$FULL_BACKUP_DIR" -maxdepth 1 -type f -mtime +30 -print -delete 2>/dev/null || true + # Cleanup old bundled backups (older than 30 days) + find "$FULL_BACKUP_DIR" -maxdepth 1 -type f -name "full_backup_*.tar.gz" -mtime +30 -print -delete 2>/dev/null || true } docker_backup() { - if ! command -v docker >/dev/null 2>&1; then - return 1 - fi - if ! docker compose -f "$COMPOSE_FILE" ps -q app >/dev/null 2>&1; then - return 1 - fi + if ! command -v docker >/dev/null 2>&1; then return 1; fi + + local timestamp="$(date +'%Y-%m-%d_%H-%M-%S')" + local staging_dir="/tmp/backup_stage_$timestamp" + + echo "[$(date +'%T')] Starting unified system backup..." + mkdir -p "$staging_dir" - local timestamp - timestamp="$(date +'%Y-%m-%d_%H-%M-%S')" - - echo "[$(date +'%T')] Starting full system backup..." - - # 1. Full MongoDB Dump + # 1. MongoDB Dump (into staging) if docker compose -f "$COMPOSE_FILE" ps -q mongodb >/dev/null 2>&1; then - echo "[$(date +'%T')] Creating MongoDB dump..." - docker compose -f "$COMPOSE_FILE" exec -T mongodb mongodump --archive --gzip > "$FULL_BACKUP_DIR/full_db_${timestamp}.archive.gz" - else - echo "Warning: MongoDB container not found. Skipping DB dump." + echo "[$(date +'%T')] Dumping Database..." + docker compose -f "$COMPOSE_FILE" exec -T mongodb mongodump --archive --gzip > "$staging_dir/db.archive.gz" fi - # 2. Archive Logs + # 2. Archive Assets (into staging) + if [ -d "./Web/uploads" ]; then + echo "[$(date +'%T')] Archiving assets..." + tar -czf "$staging_dir/assets.tar.gz" -C . Web/uploads Web/thumbnails Web/previews 2>/dev/null || true + fi + + # 3. Archive Logs (into staging) if [ -d "$LOG_DIR" ]; then echo "[$(date +'%T')] Archiving logs..." - tar -czf "$FULL_BACKUP_DIR/logs_${timestamp}.tar.gz" -C "$(dirname "$LOG_DIR")" "$(basename "$LOG_DIR")" - fi - - # 3. Legacy Invoice Export - local app_container - app_container="$(docker compose -f "$COMPOSE_FILE" ps -q app | tr -d '[:space:]')" - if [[ -n "$app_container" ]]; then - local output_dir="/tmp/invoice_archive_${timestamp}" - local remote_base="${output_dir}/${BASE_NAME}" - - docker compose -f "$COMPOSE_FILE" exec -T app mkdir -p "$output_dir" - - docker compose -f "$COMPOSE_FILE" exec -T app python3 /app/Web/modules/inventarsystem/backup_invoices.py \ - --archive-dir "$output_dir" \ - --base-name "$BASE_NAME" >/dev/null 2>&1 || true - - if docker compose -f "$COMPOSE_FILE" exec -T app test -f "${remote_base}.jsonl"; then - docker cp "$app_container":"${remote_base}.jsonl" "$INVOICE_ARCHIVE_DIR/" - docker cp "$app_container":"${remote_base}.csv" "$INVOICE_ARCHIVE_DIR/" - docker cp "$app_container":"${remote_base}.meta.json" "$INVOICE_ARCHIVE_DIR/" - else - echo "[$(date +'%T')] Info: No invoice records found to export." - fi - - docker compose -f "$COMPOSE_FILE" exec -T app rm -rf "$output_dir" + tar -czf "$staging_dir/logs.tar.gz" -C "$(dirname "$LOG_DIR")" "$(basename "$LOG_DIR")" fi + # 4. Bundle everything into one file + echo "[$(date +'%T')] Bundling archive..." + tar -czf "$FULL_BACKUP_DIR/full_backup_$timestamp.tar.gz" -C "$staging_dir" . + + # 5. Cleanup staging + rm -rf "$staging_dir" + + echo "[$(date +'%T')] Backup complete: $FULL_BACKUP_DIR/full_backup_$timestamp.tar.gz" return 0 } main() { parse_args "$@" - - if [[ -z "$BASE_NAME" ]]; then - BASE_NAME="invoices-$(date +'%Y-%m-%d_%H-%M-%S')" - fi - ensure_directory case "$MODE" in diff --git a/config.json b/config.json index a31421b..74bf877 100755 --- a/config.json +++ b/config.json @@ -27,7 +27,7 @@ "username": "", "password": "", "from_address": "", - "default_sender_name": "Invario Inventurprogramm", + "default_sender_name": "Invario Inventarprogramm", "timeout_seconds": 30 }, "images": { diff --git a/restore.sh b/restore.sh new file mode 100755 index 0000000..890be51 --- /dev/null +++ b/restore.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +BACKUP_DIR="/var/backups/inventarsystem" +COMPOSE_FILE="docker-compose-multitenant.yml" + +# 1. Find the latest backup bundle +LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/full_backup_*.tar.gz | head -n1 2>/dev/null || true) + +if [[ -z "$LATEST_BACKUP" ]]; then + echo "Error: No backup bundle found in $BACKUP_DIR" + exit 1 +fi + +echo "--- RESTORE PROCEDURE ---" +echo "Found latest bundle: $(basename "$LATEST_BACKUP")" +read -p "WARNING: This will OVERWRITE your current database and assets! Continue? (y/N) " confirm +[[ "$confirm" != "y" ]] && exit 1 + +# 2. Extract the bundle to a temporary location +RESTORE_TMP="/tmp/restore_$(date +%s)" +mkdir -p "$RESTORE_TMP" +echo "Extracting bundle..." +tar -xzf "$LATEST_BACKUP" -C "$RESTORE_TMP" + +# 3. Stop application +echo "Stopping application..." +docker compose -f "$COMPOSE_FILE" stop app + +# 4. Restore Database +if [ -f "$RESTORE_TMP/db.archive.gz" ]; then + echo "Restoring MongoDB..." + zcat "$RESTORE_TMP/db.archive.gz" | docker compose -f "$COMPOSE_FILE" exec -T mongodb mongorestore --archive --gzip --drop +else + echo "Warning: Database archive not found in bundle." +fi + +# 5. Restore Assets +if [ -f "$RESTORE_TMP/assets.tar.gz" ]; then + echo "Restoring Assets (Uploads/Thumbnails/Previews)..." + tar -xzf "$RESTORE_TMP/assets.tar.gz" -C . +else + echo "Warning: Asset archive not found in bundle." +fi + +# 6. Restore Logs (Optional) +if [ -f "$RESTORE_TMP/logs.tar.gz" ]; then + echo "Restoring Logs..." + tar -xzf "$RESTORE_TMP/logs.tar.gz" -C . +fi + +# 7. Start application +echo "Restarting application..." +docker compose -f "$COMPOSE_FILE" start app + +# 8. Cleanup +rm -rf "$RESTORE_TMP" +echo "Restore complete!" \ No newline at end of file