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