200 lines
7.4 KiB
Bash
Executable File
200 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copyright 2026 Umbra Tools (umbratools.com)
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
set -Eeuo pipefail
|
|
umask 077
|
|
|
|
WORKSPACE='/home/umbra/src/UmbraTrans'
|
|
USER_NAME='umbra'
|
|
USER_GROUP='users'
|
|
NAS_MOUNT='/run/user/1002/gvfs'
|
|
NAS_ROOT='/run/user/1002/gvfs/smb-share:server=nas2.local,share=netbackup/UmbraTrans'
|
|
BACKUP_ROOT="${NAS_ROOT}/backups"
|
|
POSTGRES_DIR="${BACKUP_ROOT}/postgres"
|
|
CONFIG_DIR="${BACKUP_ROOT}/config"
|
|
WORKSPACE_DIR="${BACKUP_ROOT}/workspace"
|
|
MANIFEST_DIR="${BACKUP_ROOT}/manifests"
|
|
DB_NAME='umbracontrol'
|
|
DB_STAGE='/var/lib/postgresql/umbratrans-backup-stage'
|
|
LOCAL_STAGE=''
|
|
DB_DUMP=''
|
|
|
|
log() {
|
|
printf 'umbratrans-backup: %s\n' "$*"
|
|
}
|
|
|
|
fail() {
|
|
printf 'umbratrans-backup: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
as_user() {
|
|
runuser -u "${USER_NAME}" -- "$@"
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -n "${DB_DUMP}" ]]; then
|
|
rm -f -- "${DB_DUMP}"
|
|
fi
|
|
if [[ -n "${LOCAL_STAGE}" ]]; then
|
|
rm -rf -- "${LOCAL_STAGE}"
|
|
fi
|
|
rmdir "${DB_STAGE}" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
require_private_directory() {
|
|
local path="$1"
|
|
as_user test -d "${path}" || fail "required directory is unavailable: ${path}"
|
|
as_user test ! -L "${path}" || fail "required path is a symlink: ${path}"
|
|
local mode
|
|
mode="$(as_user stat -c '%a' "${path}")"
|
|
(( (8#${mode} & 8#077) == 0 )) || fail "directory is not private: ${path}"
|
|
as_user test -r "${path}" || fail "directory is not readable by ${USER_NAME}: ${path}"
|
|
as_user test -w "${path}" || fail "directory is not writable by ${USER_NAME}: ${path}"
|
|
}
|
|
|
|
publish_file() {
|
|
local source_path="$1"
|
|
local destination_dir="$2"
|
|
local destination_name="$3"
|
|
local user_copy="${LOCAL_STAGE}/publish-${destination_name}"
|
|
local temporary_path="${destination_dir}/.${destination_name}.tmp"
|
|
local destination_path="${destination_dir}/${destination_name}"
|
|
local source_hash published_hash
|
|
|
|
install -o "${USER_NAME}" -g "${USER_GROUP}" -m 0600 -- "${source_path}" "${user_copy}"
|
|
as_user /usr/bin/cp -- "${user_copy}" "${temporary_path}"
|
|
as_user /bin/mv -fT -- "${temporary_path}" "${destination_path}"
|
|
|
|
source_hash="$(sha256sum "${source_path}" | awk '{print $1}')"
|
|
published_hash="$(as_user sha256sum "${destination_path}" | awk '{print $1}')"
|
|
[[ "${source_hash}" == "${published_hash}" ]] || fail "published checksum mismatch: ${destination_name}"
|
|
}
|
|
|
|
write_checksum_file() {
|
|
local source_path="$1"
|
|
local destination_name="$2"
|
|
local hash
|
|
hash="$(sha256sum "${source_path}" | awk '{print $1}')"
|
|
printf '%s %s\n' "${hash}" "${destination_name}" > "${LOCAL_STAGE}/${destination_name}.sha256"
|
|
}
|
|
|
|
exec 9>/run/lock/umbratrans-backup.lock
|
|
flock -n 9 || fail 'another backup is already running'
|
|
|
|
as_user mountpoint -q "${NAS_MOUNT}" || {
|
|
log 'confirmed NAS GVfs mount is unavailable; no backup was written'
|
|
exit 75
|
|
}
|
|
|
|
require_private_directory "${NAS_ROOT}"
|
|
require_private_directory "${BACKUP_ROOT}"
|
|
require_private_directory "${POSTGRES_DIR}"
|
|
require_private_directory "${CONFIG_DIR}"
|
|
require_private_directory "${WORKSPACE_DIR}"
|
|
require_private_directory "${MANIFEST_DIR}"
|
|
as_user test -d "${WORKSPACE}" || fail "workspace is unavailable: ${WORKSPACE}"
|
|
as_user test ! -L "${WORKSPACE}" || fail "workspace path is a symlink: ${WORKSPACE}"
|
|
|
|
[[ -x /usr/bin/pg_dump && -x /usr/bin/pg_restore && -x /usr/sbin/runuser ]] || fail 'required backup tools are unavailable'
|
|
|
|
LOCAL_STAGE="$(mktemp -d /var/tmp/umbratrans-backup.XXXXXX)"
|
|
chmod 0700 "${LOCAL_STAGE}"
|
|
chown "${USER_NAME}:${USER_GROUP}" "${LOCAL_STAGE}"
|
|
install -d -o postgres -g postgres -m 0700 "${DB_STAGE}"
|
|
|
|
run_id="$(date -u '+%Y%m%dT%H%M%SZ')-$$"
|
|
created_at="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
|
db_name="umbracontrol-${run_id}.dump"
|
|
config_name="umbracontrol-config-${run_id}.tar.gz"
|
|
workspace_name="umbratrans-workspace-${run_id}.tar.gz"
|
|
|
|
DB_DUMP="${DB_STAGE}/${db_name}"
|
|
runuser -u postgres -- /usr/bin/pg_dump \
|
|
--format=custom \
|
|
--compress=9 \
|
|
--no-owner \
|
|
--no-privileges \
|
|
--file="${DB_DUMP}" \
|
|
"${DB_NAME}"
|
|
runuser -u postgres -- /usr/bin/pg_restore --list "${DB_DUMP}" >/dev/null
|
|
install -o "${USER_NAME}" -g "${USER_GROUP}" -m 0600 -- "${DB_DUMP}" "${LOCAL_STAGE}/${db_name}"
|
|
|
|
config_stage="${LOCAL_STAGE}/postgresql-config"
|
|
install -d -m 0700 "${config_stage}"
|
|
install -m 0600 \
|
|
/etc/postgresql/16/main/postgresql.conf \
|
|
/etc/postgresql/16/main/pg_hba.conf \
|
|
/etc/postgresql/16/main/pg_ident.conf \
|
|
/etc/postgresql/16/main/start.conf \
|
|
"${config_stage}/"
|
|
tar -C "${config_stage}" -czf "${LOCAL_STAGE}/${config_name}" \
|
|
postgresql.conf pg_hba.conf pg_ident.conf start.conf
|
|
tar -tzf "${LOCAL_STAGE}/${config_name}" >/dev/null
|
|
|
|
tar -C /home/umbra/src \
|
|
--exclude='UmbraTrans/.venv' \
|
|
--exclude='UmbraTrans/.git' \
|
|
--exclude='UmbraTrans/.local' \
|
|
--exclude='UmbraTrans/agent-mailbox-local' \
|
|
--exclude='UmbraTrans/.env' \
|
|
--exclude='*.pem' \
|
|
--exclude='*.key' \
|
|
--exclude='*.token' \
|
|
--exclude='*/__pycache__' \
|
|
--exclude='*.pyc' \
|
|
-czf "${LOCAL_STAGE}/${workspace_name}" \
|
|
UmbraTrans
|
|
tar -tzf "${LOCAL_STAGE}/${workspace_name}" >/dev/null
|
|
|
|
publish_file "${LOCAL_STAGE}/${db_name}" "${POSTGRES_DIR}" "${db_name}"
|
|
write_checksum_file "${LOCAL_STAGE}/${db_name}" "${db_name}"
|
|
publish_file "${LOCAL_STAGE}/${db_name}.sha256" "${POSTGRES_DIR}" "${db_name}.sha256"
|
|
|
|
publish_file "${LOCAL_STAGE}/${config_name}" "${CONFIG_DIR}" "${config_name}"
|
|
write_checksum_file "${LOCAL_STAGE}/${config_name}" "${config_name}"
|
|
publish_file "${LOCAL_STAGE}/${config_name}.sha256" "${CONFIG_DIR}" "${config_name}.sha256"
|
|
|
|
publish_file "${LOCAL_STAGE}/${workspace_name}" "${WORKSPACE_DIR}" "${workspace_name}"
|
|
write_checksum_file "${LOCAL_STAGE}/${workspace_name}" "${workspace_name}"
|
|
publish_file "${LOCAL_STAGE}/${workspace_name}.sha256" "${WORKSPACE_DIR}" "${workspace_name}.sha256"
|
|
|
|
cat > "${LOCAL_STAGE}/postgres.manifest" <<EOF
|
|
created_at=${created_at}
|
|
database=${DB_NAME}
|
|
format=pg_dump_custom
|
|
file=${db_name}
|
|
sha256_file=${db_name}.sha256
|
|
EOF
|
|
publish_file "${LOCAL_STAGE}/postgres.manifest" "${MANIFEST_DIR}" "umbracontrol-${run_id}.manifest"
|
|
|
|
cat > "${LOCAL_STAGE}/config.manifest" <<EOF
|
|
created_at=${created_at}
|
|
scope=postgresql-config-only
|
|
file=${config_name}
|
|
sha256_file=${config_name}.sha256
|
|
EOF
|
|
publish_file "${LOCAL_STAGE}/config.manifest" "${MANIFEST_DIR}" "umbracontrol-config-${run_id}.manifest"
|
|
|
|
cat > "${LOCAL_STAGE}/workspace.manifest" <<EOF
|
|
created_at=${created_at}
|
|
scope=workspace-source-config-only
|
|
file=${workspace_name}
|
|
sha256_file=${workspace_name}.sha256
|
|
EOF
|
|
publish_file "${LOCAL_STAGE}/workspace.manifest" "${MANIFEST_DIR}" "umbratrans-workspace-${run_id}.manifest"
|
|
|
|
as_user find "${POSTGRES_DIR}" -maxdepth 1 -type f \
|
|
\( -name 'umbracontrol-*.dump' -o -name 'umbracontrol-*.dump.sha256' \) -mtime +30 -delete
|
|
as_user find "${CONFIG_DIR}" -maxdepth 1 -type f \
|
|
\( -name 'umbracontrol-config-*.tar.gz' -o -name 'umbracontrol-config-*.tar.gz.sha256' \) -mtime +30 -delete
|
|
as_user find "${WORKSPACE_DIR}" -maxdepth 1 -type f \
|
|
\( -name 'umbratrans-workspace-*.tar.gz' -o -name 'umbratrans-workspace-*.tar.gz.sha256' \) -mtime +14 -delete
|
|
as_user find "${MANIFEST_DIR}" -maxdepth 1 -type f \
|
|
\( -name 'umbracontrol-*.manifest' -o -name 'umbracontrol-config-*.manifest' -o -name 'umbratrans-workspace-*.manifest' \) -mtime +30 -delete
|
|
as_user find "${BACKUP_ROOT}" -type f -name '.*.tmp' -mmin +60 -delete
|
|
|
|
log "completed ${created_at}; PostgreSQL, configuration, and workspace verified"
|