diff --git a/.metadata b/.metadata index 782f498..13935af 100644 Binary files a/.metadata and b/.metadata differ diff --git a/.templates/nginx/nginx_xport.inc b/.templates/nginx/nginx_xport.inc new file mode 100644 index 0000000..66362d6 --- /dev/null +++ b/.templates/nginx/nginx_xport.inc @@ -0,0 +1,18 @@ + # Includable nginx configuration. + # + # Export backups feature. + # Needs + # setfacl -m u:www-data:r [...]/configs/xport_backup + # setfacl -m u:www-data:rx [...]/storage/backups + # setfacl -d -m u:www-data:r [...]/storage/backups/export + # ACLs. + location /export { + root $PAR_SERVICE/storage/backups; + auth_basic "Export backups area"; + auth_basic_user_file $PAR_SERVICE/configs/xport_backup; + allow all; + autoindex on; + autoindex_exact_size off; + autoindex_format html; + autoindex_localtime on; + } diff --git a/configs/.gitignore b/configs/.gitignore index 5daa722..7b9b5b6 100644 --- a/configs/.gitignore +++ b/configs/.gitignore @@ -1,4 +1,5 @@ # Ignore everything else in this directory. * !certs +!xport_backup !.gitignore diff --git a/configs/xport_backup b/configs/xport_backup new file mode 100644 index 0000000..af5f082 --- /dev/null +++ b/configs/xport_backup @@ -0,0 +1,3 @@ +# Credentials file for exported backups feature. +# Needs username:apr1-hashed password entries, one per line. +# Use https://htpasswd.utils.com/ or some similar to fill in. diff --git a/storage/.gitignore b/storage/.gitignore new file mode 100644 index 0000000..171ea04 --- /dev/null +++ b/storage/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory except this folders. +* +!.gitignore +!backups +!volumes diff --git a/storage/backups/.gitignore b/storage/backups/.gitignore new file mode 100644 index 0000000..c8a5ef2 --- /dev/null +++ b/storage/backups/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory except these files. +* +!.gitignore +!export +!tarballs diff --git a/storage/backups/export/.gitignore b/storage/backups/export/.gitignore new file mode 100644 index 0000000..4a4fa60 --- /dev/null +++ b/storage/backups/export/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory except this file. +* +!.gitignore +!.rotate_folder.conf diff --git a/storage/backups/export/.rotate_folder.conf b/storage/backups/export/.rotate_folder.conf new file mode 100644 index 0000000..581a596 --- /dev/null +++ b/storage/backups/export/.rotate_folder.conf @@ -0,0 +1,9 @@ +# This is a shell script excerpt for configuration purposes only. +# Handle with care! Please don't put code here, only variables. + +CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$" +DOIT="yes" # if empty the script makes a dry run +RETAIN_DAYS=7 # retains all files created within that many days +RETAIN_WEEKS=0 # retains one file per week/month, +RETAIN_MONTHS=0 # created within that many weeks/months + diff --git a/tools/backup.d/xport_backup.sh b/tools/backup.d/xport_backup.sh new file mode 100644 index 0000000..953277d --- /dev/null +++ b/tools/backup.d/xport_backup.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# +# Optional additional backup operation, intended to export an (almost) +# up-to-date downloadable copy for our customers about their data +# handled by us. The script synchronizes some of the existing backup +# files to an export folder that can be downloaded from the web. +# +# Uses the rotate_folder tool to select files to synchronize. +# This tool must be somewhere in the path. +# +# Author: Kovács Zoltán +# License: GNU/GPL v3+ (https://www.gnu.org/licenses/gpl-3.0.en.html) +# 2025-03-06 v0.1 Initial release + +# Accepted environment variables and their defaults. +PAR_BASEDIR=${PAR_BASEDIR:-""} # Service's base folder +PAR_DUMPDIR=${PAR_DUMPDIR:-""} # Absolute path to DB dumps +PAR_EXPORTDIR=${PAR_EXPORTDIR:-""} # Absolute path to export dir +PAR_RETAINDAYS=${PAR_RETAINDAYS:-"1"} # Days to retain the copies +PAR_TARBALLDIR=${PAR_TARBALLDIR:-""} # Absolute path to tgz dumps + +# Other initialisations. +CLASSES_PATTERN="^([^.]*)\..*\.$HOSTNAME\.(dmp|sql\.gz|tgz|log)$" +DUMPPATH="storage/backups/dumps" # Default path to DB dumps +EXPORTPATH="storage/backups/export" # Default path to export dir +TARBALLPATH="storage/backups/tarballs" # Default path to tgz dumps +USER=${USER:-LOGNAME} # Fix for cron enviroment only +YMLFILE="docker-compose.yml" + +# Messages. +MSG_MISSINGDEP="Fatal: missing dependency" +MSG_MISSINGYML="Fatal: didn't find the docker-compose.yml file" +MSG_NONWRITE="The target directory isn't writable" + +# Checks the dependencies. +TR=$(which tr 2>/dev/null) +if [ -z "$TR" ]; then echo "$MSG_MISSINGDEP tr."; exit 1 ; fi +for item in cp cut date dirname grep hostname readlink rotate_folder tar +do + if [ -n "$(which $item)" ] + then export $(echo $item | "$TR" '[:lower:]' '[:upper:]' | "$TR" '-' '_')=$(which $item) + else echo "$MSG_MISSINGDEP $item." >&2; exit 1; fi +done + +# Where I'm? +# https://gist.github.com/TheMengzor/968e5ea87e99d9c41782 +SOURCE="$0" +while [ -h "$SOURCE" ]; do + # resolve $SOURCE until the file is no longer a symlink + SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && pwd )" #" + SOURCE="$("$READLINK" "$SOURCE")" + # if $SOURCE was a relative symlink, we need to resolve it + # relative to the path where the symlink file was located + [[ $SOURCE != /* ]] && SOURCE="$SCRPATH/$SOURCE" +done; SCRPATH="$( cd -P "$("$DIRNAME" "$SOURCE" )" && pwd )" #" + +# Searches the base folder, containing a docker-compose.yml file. +# Called from the base folder (./)? +BASE_DIR="$PAR_BASEDIR" +TEST_DIR="$SCRPATH" +[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR" +# Called from ./tools? +TEST_DIR="$("$DIRNAME" "$TEST_DIR")" +[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR" +# Called from ./tools/*.d? +TEST_DIR="$("$DIRNAME" "$TEST_DIR")" +[[ -z "$BASE_DIR" ]] && [[ -r "$TEST_DIR/$YMLFILE" ]] && BASE_DIR="$TEST_DIR" +# On failure gives it up here. +if [ -z "$BASE_DIR" -o ! -r "$BASE_DIR/$YMLFILE" ]; then + echo "$MSG_MISSINGYML" >&2; exit 1 +fi +# Sets the absolute paths. +DUMPDIR="${PAR_DUMPDIR:-$BASE_DIR/$DUMPPATH}" +EXPORTDIR="${PAR_EXPORTDIR:-$BASE_DIR/$EXPORTPATH}" +TARBALLDIR="${PAR_TARBALLDIR:-$BASE_DIR/$TARBALLPATH}" + +# Exits silently if EXPORTDIR isn't present. +[[ ! -e "$EXPORTDIR" ]] && exit 0 +# EXPORTDIR must be writable. +[[ ! -w "$EXPORTDIR" ]] \ +&& echo "$MSG_NONWRITE: $BACKUPDIR" >&2 && exit 1 + +# Let's select and copy the appropriate backup files. +# +# We'll call rotate_folder (dry run) with CLASSES_PATTERN and PAR_RETAINDAYS +# set above to select relevant files created in the backup folders within last +# PAR_RETAINDAYS days. These files are synchronized with the cp -u statement. +# +# Enumerates the folders. +for folder in "$DUMPDIR" "$TARBALLDIR" +do + # Selects the appropriate files (which have the "DR" - daily retain - tag). + for filename in $((export CLASSES_PATTERN="$CLASSES_PATTERN" \ + RETAIN_DAYS="$PAR_RETAINDAYS" RETAIN_WEEKS=0 RETAIN_MONTHS=0; \ + "$ROTATE_FOLDER" --noconf -f "$folder") | \ + "$GREP" '^DR ' | "$CUT" -d' ' -f2) "" + do + # Updates the current file. + if [ -n "$filename" ]; then + "$CP" -u "$folder/$filename" "$EXPORTDIR/" 2>/dev/null + fi + done +done + +# That's all, Folks! :)