Skip to content

Repo Common

Konfiguracja

Skrypt zawiera funkcje współdzielone przez skrypty synchronizacji oraz odtwarzania repozytorium (wykluczenia, logowanie, blokady, retencja, walidacja konfiguracji). Jest potrzebny do ujednolicenia zachowania i ograniczenia powielania logiki.

Założenia konfiguracyjne:

  • Skrypty znajdują się w /srv/config/scripts/
  • Logi skryptów trafiają na stdout/stderr
  • Pozostałe skrypty korzystające z pliku: repo-sync.sh, repo-restore.sh
  • Katalog blokad znajduje się w /srv/backups/.locks

Tworzenie skryptu

Tworzymy plik:

micro /srv/config/scripts/repo-common.sh

W pliku umieszczamy:

repo-common.sh
#!/usr/bin/env bash
# /srv/config/scripts/repo-common.sh
# Common helpers for repo-sync / repo-restore scripts.

set -euo pipefail
IFS=$'\n\t'

# ANSI Colors
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[31m'
NC='\033[0m'

# Globals
CURRENT_RUN_DIR=""
LOCK_BASE_DIR="/srv/backups/.locks"   # Shared lock dir (safe, root-owned)
REPO_LOCK_FD=""                       # Dynamic FD holder (keeps lock alive)

# Default rsync excludes (shared policy)
RSYNC_EXCLUDES=(
  "--exclude=.git/"
  "--exclude=.env"
  "--exclude=**/.env"
  "--exclude=**/data/"
  "--exclude=**/volumes/"
  "--exclude=**/logs/"
  "--exclude=**/backups/"
  "--exclude=**/*.db"
  "--exclude=**/*.sqlite*"
  "--exclude=**/node_modules/"
  "--exclude=**/dist/"
  "--exclude=**/build/"
  "--exclude=**/.next/"
  "--exclude=**/.turbo/"
  "--exclude=**/.cache/"
  "--exclude=**/.vite/"
  "--exclude=**/coverage/"
  "--exclude=**/*.log"
  "--exclude=**/.terraform/"
  "--exclude=**/.venv/"
  "--exclude=**/venv/"
  "--exclude=**/env/"
  "--exclude=**/__pycache__/"
  "--exclude=**/*.pyc"
)

# --- Logging ---
log()      { echo -e "${GREEN}[repo]${NC} $1"; }
header()   { echo -e "\n${CYAN}=== $1 ===${NC}"; }
warn()     { echo -e "${YELLOW}Warning: $1${NC}"; }
error()    { echo -e "${RED}Error: $1${NC}" >&2; }
critical() { echo -e "${RED}CRITICAL ERROR: $1${NC}" >&2; exit 1; }

# --- Basic checks ---
check_root() {
  if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
    critical "Run as root (use sudo)."
  fi
}

require_nonempty() {
  local val="${1:-}"
  local name="${2:-var}"
  [[ -n "$val" ]] || critical "${name} is empty (safety stop)."
}

require_dir() {
  local d="$1"
  [[ -d "$d" ]] || critical "Directory missing: $d"
}

# Ensure path is under a given prefix (prevents dangerous deletes/copies).
require_under() {
  local base="$1"
  local path="$2"

  require_nonempty "$base" "base"
  require_nonempty "$path" "path"

  if [[ "$path" != "$base" && "$path" != "$base/"* ]]; then
    critical "SAFETY STOP: Path '$path' is outside '$base'."
  fi
}

# --- Config file hardening ---
check_config_perms() {
  local config_file="$1"

  [[ -f "$config_file" ]] || critical "Config file missing: $config_file"
  [[ -L "$config_file" ]] && critical "Config file is a symlink (unsafe): $config_file"

  # Strict check: owned by root:root and mode 600
  if [[ "$(stat -c '%U:%G %a' "$config_file")" != "root:root 600" ]]; then
    critical "Unsafe permissions for $config_file (expected root:root 600)"
  fi
}

# Load env file after permission checks.
# Note: does NOT validate individual variables; scripts should do that.
load_repo_env() {
  local env_file="$1"
  check_config_perms "$env_file"

  set -a
  # shellcheck disable=SC1090
  source "$env_file"
  set +a
}

# --- Locking (prevents parallel execution) ---
# Exit code: 2 when already running (useful for timers).
acquire_lock() {
  local lock_name="$1"
  require_nonempty "$lock_name" "lock_name"

  install -d -m 0755 "$LOCK_BASE_DIR" || critical "Cannot create lock dir: $LOCK_BASE_DIR"
  local lock_file="${LOCK_BASE_DIR}/${lock_name}.lock"

  # Dynamic FD avoids collisions.
  exec {REPO_LOCK_FD}>"$lock_file"

  if ! flock -n "$REPO_LOCK_FD"; then
    error "Script is already running. Locked: $lock_file"
    exit 2
  fi
}

# --- Run dir + failure marker ---
_repo_exit_handler() {
  local rc=$?
  if [[ $rc -ne 0 ]] && [[ -n "${CURRENT_RUN_DIR}" ]]; then
    : > "${CURRENT_RUN_DIR}/.FAILED"
    echo "rc=${rc}" > "${CURRENT_RUN_DIR}/exit-code.txt" 2>/dev/null || true
    error "Run failed (rc=$rc). See logs in: ${CURRENT_RUN_DIR}"
  fi
}

setup_run_env() {
  local run_dir="$1"
  require_nonempty "$run_dir" "run_dir"

  CURRENT_RUN_DIR="$run_dir"
  mkdir -p "$run_dir"
  trap _repo_exit_handler EXIT
}

finish_run() {
  local run_dir="$1"
  require_nonempty "$run_dir" "run_dir"

  : > "${run_dir}/.SUCCESS"
  log "Completed successfully."
  echo "FINAL_OUT_DIR=${run_dir}"
}

# --- Safe retention (for run dirs) ---
perform_retention() {
  local target_dir="$1"
  local days="$2"

  header "Retention Policy"

  require_nonempty "$target_dir" "target_dir"
  require_nonempty "$days" "days"

  # Safety: only allow deletions under /srv/backups
  require_under "/srv/backups" "$target_dir"

  if [[ ! -d "$target_dir" ]]; then
    warn "Retention: Directory $target_dir does not exist. Skipping."
    return
  fi

  log "Cleaning directories older than $days days in: $target_dir"

  # Match timestamp run dirs: 20YYYYMMDD_HHMM
  find "$target_dir" -maxdepth 1 -mindepth 1 -type d \
    -name '20????????_????' \
    -mtime +"$days" \
    -print -exec rm -rf {} +
}

# --- Metadata (no secrets) ---
write_env_summary() {
  local run_dir="$1"
  shift

  require_nonempty "$run_dir" "run_dir"
  mkdir -p "$run_dir"

  : > "${run_dir}/env-summary.txt"
  for kv in "$@"; do
    echo "$kv" >> "${run_dir}/env-summary.txt"
  done
}

# --- Convenience: tool checks ---
require_cmd() {
  local cmd="$1"
  command -v "$cmd" >/dev/null 2>&1 || critical "Command not found in PATH: $cmd"
}

Nadawanie uprawnień

Uprawnienia 0640 ograniczają zapis wyłącznie do konta root, a odczyt udostępniają tylko rootowi i uprzywilejowanej grupie (np. sudo).

sudo chown root:root /srv/config/scripts/repo-common.sh
sudo chmod 0640 /srv/config/scripts/repo-common.sh