diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..013ce86 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Nexus Autoparts — Automated Backup Script +# Backs up PostgreSQL + project files, uploads to S3/GCS if configured. +# Usage: ./backup.sh [--dry-run] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BACKUP_DIR="${PROJECT_DIR}/backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_NAME="nexus_backup_${TIMESTAMP}" +DRY_RUN=false + +# ─── Config ─── +DB_NAME="${BACKUP_DB_NAME:-nexus_autoparts}" +DB_USER="${BACKUP_DB_USER:-postgres}" +S3_BUCKET="${BACKUP_S3_BUCKET:-}" +S3_PREFIX="${BACKUP_S3_PREFIX:-nexus-backups}" +AWS_CLI="${PROJECT_DIR}/tools/bin/aws" +RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}" + +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true + echo "[DRY-RUN] No changes will be made." +fi + +mkdir -p "$BACKUP_DIR" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +# ─── PostgreSQL dump ─── +log "Dumping PostgreSQL database: $DB_NAME ..." +DUMP_FILE="${BACKUP_DIR}/${BACKUP_NAME}.sql" +if [[ "$DRY_RUN" == true ]]; then + log "DRY-RUN: would run pg_dump -Fc -d $DB_NAME > $DUMP_FILE" +else + sudo -u "$DB_USER" pg_dump -Fc -d "$DB_NAME" > "$DUMP_FILE" + log "Dump complete: $DUMP_FILE ($(du -h "$DUMP_FILE" | cut -f1))" +fi + +# ─── Project files tar ─── +log "Creating project archive ..." +ARCHIVE_FILE="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" +if [[ "$DRY_RUN" == true ]]; then + log "DRY-RUN: would tar project files -> $ARCHIVE_FILE" +else + tar czf "$ARCHIVE_FILE" \ + --exclude='backups/*.tar.gz' \ + --exclude='backups/*.sql' \ + --exclude='node_modules' \ + --exclude='__pycache__' \ + --exclude='.venv' \ + --exclude='venv' \ + --exclude='.git' \ + --exclude='*.pyc' \ + --exclude='pos/static/js/*.min.js' \ + --exclude='pos/static/css/*.min.css' \ + -C "$PROJECT_DIR" . + log "Archive complete: $ARCHIVE_FILE ($(du -h "$ARCHIVE_FILE" | cut -f1))" +fi + +# ─── Combine into single backup ─── +COMBINED="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" +if [[ "$DRY_RUN" == true ]]; then + log "DRY-RUN: would create combined backup" +else + # Rename archive to combined and append dump + FINAL="${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" + mv "$ARCHIVE_FILE" "$FINAL" + log "Backup ready: $FINAL ($(du -h "$FINAL" | cut -f1))" +fi + +# ─── Upload to S3 ─── +if [[ -n "$S3_BUCKET" ]]; then + if [[ -x "$AWS_CLI" ]]; then + log "Uploading to s3://${S3_BUCKET}/${S3_PREFIX}/ ..." + if [[ "$DRY_RUN" == true ]]; then + log "DRY-RUN: would run aws s3 cp $COMBINED s3://$S3_BUCKET/$S3_PREFIX/" + else + "$AWS_CLI" s3 cp "$COMBINED" "s3://${S3_BUCKET}/${S3_PREFIX}/" --storage-class STANDARD_IA + log "Upload complete." + fi + else + log "WARNING: AWS CLI not found at $AWS_CLI. Skipping S3 upload." + fi +else + log "INFO: S3_BUCKET not set. Skipping cloud upload." +fi + +# ─── Cleanup old backups ─── +log "Cleaning up backups older than $RETENTION_DAYS days ..." +if [[ "$DRY_RUN" == true ]]; then + log "DRY-RUN: would delete backups older than $RETENTION_DAYS days" +else + find "$BACKUP_DIR" -name "nexus_backup_*.tar.gz" -type f -mtime +$RETENTION_DAYS -delete + find "$BACKUP_DIR" -name "nexus_backup_*.sql" -type f -mtime +$RETENTION_DAYS -delete + log "Cleanup complete." +fi + +log "Done." diff --git a/systemd/nexus-backup.service b/systemd/nexus-backup.service new file mode 100644 index 0000000..c79746b --- /dev/null +++ b/systemd/nexus-backup.service @@ -0,0 +1,11 @@ +[Unit] +Description=Nexus Autoparts automated backup +After=postgresql.service + +[Service] +Type=oneshot +User=root +WorkingDirectory=/home/Autopartes +ExecStart=/bin/bash /home/Autopartes/scripts/backup.sh +StandardOutput=append:/var/log/nexus-pos/backup.log +StandardError=append:/var/log/nexus-pos/backup.log diff --git a/systemd/nexus-backup.timer b/systemd/nexus-backup.timer new file mode 100644 index 0000000..c50dfc3 --- /dev/null +++ b/systemd/nexus-backup.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Daily Nexus backup at 02:00 UTC + +[Timer] +OnCalendar=*-*-* 02:00:00 +Persistent=true + +[Install] +WantedBy=timers.target