initial commit

This commit is contained in:
2025-11-25 12:27:53 +03:30
commit f9d16ab078
102 changed files with 11156 additions and 0 deletions

197
scripts/backup/manage-backups.sh Executable file
View File

@@ -0,0 +1,197 @@
#!/bin/sh
set -eu
# Minimal PATH for cron-like environments
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
log() {
printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$*"
}
err() {
log "ERROR: $*" >&2
}
log "manage-backups.sh starting"
BACKUP_BASE=/backups
date_formatted() {
local format="${1:-%F_%H-%M}"
# Determine timezone to use. Priority:
# 1. $TIMEZONE environment variable
# 2. /etc/timezone file (common in many images)
# 3. /etc/localtime symlink target (if present and pointing into zoneinfo)
# 4. fall back to UTC
local tz="${TIMEZONE:-}"
if [ -z "$tz" ] && [ -f /etc/timezone ]; then
tz=$(cat /etc/timezone 2>/dev/null || true)
fi
if [ -z "$tz" ] && [ -L /etc/localtime ]; then
# readlink output might be like /usr/share/zoneinfo/Region/City
local target
target=$(readlink /etc/localtime 2>/dev/null || true)
case "$target" in
*/usr/share/zoneinfo/*) tz=${target#*/usr/share/zoneinfo/} ;;
*) tz="" ;;
esac
fi
# If still empty, try reading the container PID 1 environment (docker-compose env vars live there)
if [ -z "$tz" ] && [ -r /proc/1/environ ]; then
tz=$(tr '\0' '\n' < /proc/1/environ 2>/dev/null | sed -n 's/^TIMEZONE=//p' | head -n1 || true)
fi
if [ -n "$tz" ]; then
# Prefer the system's zoneinfo if available. We consider TZ working
# if the timezone offset (%%z) differs from UTC's offset.
local utc_z tz_z
utc_z=$(date -u +%z)
tz_z=$(TZ="$tz" date +%z 2>/dev/null || true)
if [ -n "$tz_z" ] && [ "$tz_z" != "$utc_z" ]; then
TZ="$tz" date +"$format"
return
fi
# Fallback: some minimal images (alpine without tzdata) don't have
# zoneinfo. As a pragmatic fallback map a few common timezones to
# their current standard offsets (in seconds). This is best-effort
# and does not handle historical DST transitions.
local offset_secs=0
case "$tz" in
Asia/Tehran) offset_secs=12600 ;; # +03:30
Asia/Kolkata) offset_secs=19800 ;; # +05:30
Europe/London) offset_secs=0 ;; # UTC (note: ignores BST)
Europe/Paris) offset_secs=3600 ;; # +01:00 (ignores CEST)
America/New_York) offset_secs=-18000 ;; # -05:00 (ignores EDT)
America/Los_Angeles) offset_secs=-28800 ;; # -08:00 (ignores PDT)
UTC|Etc/UTC) offset_secs=0 ;;
*) offset_secs=0 ;;
esac
# Compute local epoch by adding offset to UTC epoch and format via UTC
local epoch_utc epoch_local
epoch_utc=$(date -u +%s)
epoch_local=$((epoch_utc + offset_secs))
# Most busybox/git images support -d "@SECONDS" with -u
date -u -d "@${epoch_local}" +"$format"
else
date +"$format"
fi
}
wait_for_database_backups() {
local timeout="${DB_BACKUP_TIMEOUT:-600}"
local start_ts
start_ts=$(date -u +%s)
local status_file="$BACKUP_BASE/databases/.last_backup_complete"
log "Waiting for database backup completion marker at $status_file (timeout ${timeout}s)"
while :; do
if [ -f "$status_file" ]; then
local last_ts
last_ts=$(head -n1 "$status_file" 2>/dev/null || echo 0)
case "$last_ts" in
(""|*[!0-9]*) last_ts=0 ;;
esac
if [ "$last_ts" -ge "$start_ts" ]; then
log "Detected recent database backup completion at epoch $last_ts"
return 0
fi
fi
local now
now=$(date -u +%s)
if [ $(( now - start_ts )) -ge "$timeout" ]; then
err "Timed out waiting for database backups to finish"
return 1
fi
sleep 5
done
}
cleanup_old_backups() {
local retention_days="${BACKUP_RETENTION_DAYS:-7}"
local retention_count="${BACKUP_RETENTION_COUNT:-0}"
case "$retention_days" in
''|*[!0-9]*) retention_days=7 ;;
esac
case "$retention_count" in
''|*[!0-9]*) retention_count=0 ;;
esac
log "Cleaning up old backups older than ${retention_days} days (keep ${retention_count} newest)"
if [ "$retention_days" -ge 0 ] 2>/dev/null; then
find "$BACKUP_BASE" -maxdepth 1 -mindepth 1 -type d ! -name databases -mtime +"${retention_days}" -exec rm -rf {} \; 2>/dev/null || true
find "$BACKUP_BASE/databases" -type f -mtime +"${retention_days}" -delete 2>/dev/null || true
fi
if [ "$retention_count" -gt 0 ] 2>/dev/null; then
local idx=0
local entry
local old_ifs="$IFS"
IFS='
'
set -- $(ls -1dt "$BACKUP_BASE"/*/ 2>/dev/null || echo)
IFS="$old_ifs"
for entry in "$@"; do
[ -z "$entry" ] && continue
case "$entry" in
"$BACKUP_BASE/databases"|"$BACKUP_BASE/databases/")
continue
;;
esac
idx=$((idx + 1))
if [ "$idx" -le "$retention_count" ]; then
continue
fi
log "Removing old backup directory $entry (exceeds retention count)"
rm -rf "$entry" 2>/dev/null || err "Failed to remove $entry"
done
fi
}
# Prepare dated backup directory
DATED_DIR="$BACKUP_BASE/$(date_formatted)"
mkdir -p "$DATED_DIR"
wait_for_database_backups || exit 1
# Archive application data
log "Archiving data paths..."
# Archive multiple paths if they exist. Keeps one archive per path.
archive_path() {
local src="$1" prefix="$2"
if [ -d "$src" ]; then
log "Archiving $src"
if bsdtar --xattrs --same-owner --numeric-owner -czf "$DATED_DIR/${prefix}_$(date_formatted).tar.gz" -C "$src" .; then
log "$src archived successfully"
# (reverted) do not force ownership changes here
else
err "Failed to archive $src"
return 1
fi
else
log "Source path $src not found; skipping"
fi
}
# Prefer canonical paths mounted into the backup-manager container
archive_path /odoo_db_data odoo_db_data || true
archive_path /odoo_config odoo_config || true
archive_path /gitea_data gitea_data || true
archive_path /opencloud_data opencloud_data || true
archive_path /opencloud_config opencloud_config || true
# Find and Move today's database dumps to dated directory
log "Moving files from database dump to dated directory..."
# list the names of the files to move
#log "Files to move:"
ls -1 "$BACKUP_BASE/databases"/* 2>/dev/null || true
log "Moving database dumps to dated directory..."
mv "$BACKUP_BASE/databases"/*_$(date_formatted)*.sql.gz "$DATED_DIR"/ 2>/dev/null || true
cleanup_old_backups
# (reverted) do not change ownership of the dated directory
log "manage-backups.sh finished successfully"