#!/bin/sh ####### ### Backup Seafile/Seafile Pro server on bare-metal (NOT DOCKER) ####### ### text formatting presents norm=$(tput sgr0) err=$(tput bold)$(tput setaf 1) warn=$(tput bold)$(tput setaf 3) ok=$(tput setaf 2) yellow=$(tput setaf 3) cyan=$(tput setaf 6) magenta=$(tput setaf 5) ### functions # bad configuration value passed in details file badDetails () { if [ "$1" = "empty" ]; then exitError 130 "details:${2} cannot be NULL (undefined)" elif [ "$1" = "dne" ]; then exitError 131 "details:${2} file or directory does not exist." fi } # bad parameter passed to script badParam () { if [ "$1" = "dne" ]; then printf "${err}\nError: '%s %s'\n" "$2" "$3" printf "file or directory does not exist.${norm}\n\n" exit 1 elif [ "$1" = "empty" ]; then printf "${err}\nError: '%s' cannot have a NULL (empty) value.\n" "$2" printf "${cyan}Please use '--help' for assistance${norm}\n\n" exit 1 elif [ "$1" = "svc" ]; then printf "${err}\nError: '%s %s': Service does not exist!${norm}\n\n" \ "$2" "$3" exit 1 fi } # cleanup cleanup () { # cleanup 503 if copied if [ "$err503Copied" -eq 1 ]; then if ! rm -f "$webroot/$err503File" 2>>"$logFile"; then printf "${warn}[%s] -- [WARNING] Could not remove 503 error page." \ "$(stamp)" >> "$logFile" printf " Web interface will not function until this file is " \ >> "$logFile" printf "removed --${norm}\n" >> "$logFile" warnCount=$((warnCount+1)) else printf "${cyan}[%s] -- [INFO] 503 error page removed --${norm}\n" \ "$(stamp)" >> "$logFile" fi fi if [ "$sqlCopied" -eq 1 ]; then if ! rm -rf "$sqlDumpDir" 2>>"$logFile"; then printf "${warn}[%s] -- [WARNING] Could not remove temporary " \ "$(stamp)" >> "$logFile" printf "SQL dump directory at %s. " "$sqlDumpDir" >> "$logFile" printf "Remove manually to free up space.${norm}\n" >> "$logFile" warnCount=$((warnCount+1)) else printf "${cyan}[%s] -- [INFO] Temporary SQL dump directory " \ "$(stamp)" >> "$logFile" printf "removed --${norm}\n" >> "$logFile" fi fi if [ "$offlineBackup" -eq 1 ]; then printf "${cyan}[%s] -- [INFO] Starting seafile services --${norm}\n" \ "$(stamp)" >> "$logFile" seafSvc start fi } exitError () { printf "${err}[%s] -- [ERROR] %s: %s --${norm}\n" \ "$(stamp)" "$1" "$2" >> "$logFile" cleanup exit "$1" } # control seafile services (systemd) seafSvc () { if [ "$1" = "start" ]; then if ! systemctl start "${seafService}" >> "$logFile" 2>&1; then exitError 100 "Could not start ${seafService}" else printf "${cyan}[%s] -- [INFO] Started service: %s --${norm}\n" \ "$(stamp)" "$seafService" >> "$logFile" fi if ! systemctl start "${seafHub}" >> "$logFile" 2>&1; then exitError 101 "Could not start ${seafHub}" else printf "${cyan}[%s] -- [INFO] Started service: %s --${norm}\n" \ "$(stamp)" "$seafHub" >> "$logFile" fi elif [ "$1" = "stop" ]; then if ! systemctl stop "${seafHub}" >> "$logFile" 2>&1; then exitError 103 "Could not stop ${seafHub}" else printf "${cyan}[%s] -- [INFO] Stopped service: %s --${norm}\n" \ "$(stamp)" "$seafService" >> "$logFile" fi if ! systemctl stop "${seafService}" >> "$logFile" 2>&1; then exitError 102 "Could not stop ${seafService}" else printf "${cyan}[%s] -- [INFO] Stopped service: %s --${norm}\n" \ "$(stamp)" "$seafHub" >> "$logFile" fi fi } # generate dynamic timestamps stamp () { (date +%F" "%T) } ### end of functions ### default variable values ## script related # store logfile in the same directory as this script file using the same file # name as the script but with the extension '.log' scriptPath="$( CDPATH='' cd -- "$( dirname -- "$0" )" && pwd -P )" scriptName="$( basename "$0" )" logFile="$scriptPath/${scriptName%.*}.log" warnCount=0 configDetails="$scriptPath/seafbackup.details" err503Copied=0 sqlCopied=0 includeXtra=0 exclusions=0 # borg output verbosity -- normal borgCreateParams='--stats' borgPruneParams='--list' # 503 related use503=0 err503Path="$scriptPath/503_backup.html" err503File="${err503Path##*/}" webroot="/usr/share/nginx/html" # seafile related offlineBackup=0 seafService="seafile.service" seafHub="seahub.service" seafDir="/opt/seafile" seafData="/var/seafile" ### process startup parameters while [ $# -gt 0 ]; do case "$1" in -h|-\?|--help) # display help scriptHelp exit 0 ;; -l|--log) # set log file location if [ -n "$2" ]; then logFile="${2%/}" shift else badParam empty "$@" fi ;; -v|--verbose) # set verbose logging from borg borgCreateParams='--list --stats' borgPruneParams='--list' ;; -c|--config|--details) # location of config details file if [ -n "$2" ]; then if [ -f "$2" ]; then configDetails="${2%/}" shift else badParam dne "$@" fi else badParam empty "$@" fi ;; -5|--use-503) # enable copying 503 error page to webroot use503=1 ;; --503-path) # FULL path to 503 file if [ -n "$2" ]; then if [ -f "$2" ]; then err503Path="${2%/}" err503File="${2##*/}" shift else badParam dne "$@" fi else badParam empty "$@" fi ;; -w|--webroot) # path to webroot (copy 503) if [ -n "$2" ]; then if [ -d "$2" ]; then webroot="${2%/}" shift else badParam dne "$@" fi else badParam empty "$@" fi ;; -p|--seaf|--seafdir) # path to seafile program directory if [ -n "$2" ]; then if [ -d "$2" ]; then seafDir="${2%/}" shift else badParam dne "$@" fi else badParam empty "$@" fi ;; -d|--data|--datadir|--seafdata) # path to seafile data directory if [ -n "$2" ]; then if [ -d "$2" ]; then seafData="${2%/}" shift else badParam dne "$@" fi else badParam empty "$@" fi ;; --seafile-service) # name of seafile service if [ -n "$2" ]; then if ! systemctl list-unit-files | grep -Eq "^$2 |^$2.service" then badParam svc "$@" else seafService="${2}" shift fi else badParam empty "$@" fi ;; --seahub-service) # name of seahub service if [ -n "$2" ]; then if ! systemctl list-unit-files | grep -Eq "^$2 |^$2.service" then badParam svc "$@" else seafHub="${2}" shift fi else badParam empty "$@" fi ;; -o|--offline) # shutdown seafile during backup offlineBackup=1 ;; *) printf "${err}\nUnknown option: %s\n" "$1" printf "${cyan}Use '--help' for valid options.{$norm}\n\n" exit 1 ;; esac shift done ### check pre-requisites and default values # does the details file exist? if [ ! -f "$configDetails" ]; then badParam dne "(--details default)" "$configDetails" fi # is borg installed? if ! command -v borg > /dev/null; then printf "\n${err}ERROR: BORG is not installed on this system!${norm}\n\n" exit 3 fi # seafile directories if [ ! -d "$seafDir" ]; then badParam dne "(--seafdir default)" "$seafDir" elif [ ! -d "$seafData" ]; then badParam dne "(--seafdata default)" "$seafData" fi # offline backup if [ "$offlineBackup" -eq 1 ]; then if ! systemctl list-unit-files | \ grep -Eq "^$seafService |^$seafService.service" then badParam svc "(--seafile-service default)" "$seafService" elif ! systemctl list-unit-files | \ grep -Eq "^$seafHub |^$seafHub.service" then badParam svc "(--seahub-service default)" "$seafHub" fi fi # if 503 functionality is enabled, do 503 related files exist? if [ "$use503" -eq 1 ]; then if [ ! -f "$err503Path" ]; then badParam dne "(--503-path default)" "$err503Path" elif [ ! -d "$webroot" ]; then badParam dne "(--webroot default)" "$webroot" fi fi ### start logging printf "${magenta}[%s] --- Start %s execution ---${norm}\n" \ "$(stamp)" "$scriptName" >> "$logFile" printf "${cyan}[%s] -- [INFO] Log located at ${yellow}%s${cyan} --${norm}\n" \ "$(stamp)" "$logFile" >> "$logFile" ### 503 functionality if [ "$use503" -eq 1 ]; then printf "${cyan}[%s] -- [INFO] Copying 503 error page to " \ "$(stamp)" >> "$logFile" printf "webroot -- ${norm}\n" >> "$logFile" if ! \cp --force "${err503Path}" "${webroot}/${err503File}" 2>> "$logFile" then printf "${warn}[%s] -- [WARNING] Failed to copy 503 error page. " \ "$(stamp)" >> "$logFile" printf "Web users will NOT be notified --${norm}\n" >> "$logFile" warnCount=$((warnCount+1)) else printf "${ok}[%s] -- [SUCCESS] 503 error page copied --${norm}\n" \ "$(stamp)" >> "$logFile" # set cleanup flag err503Copied=1 fi fi ### stop seahub and seafile service if offline backup requested if [ "$offlineBackup" -eq 1 ]; then printf "${cyan}[%s] -- [INFO] Stopping seafile services --${norm}\n" \ "$(stamp)" >> "$logFile" seafSvc stop fi ### read details file to get variables needed to dump sql and run borg # check if config details file was provided as a relative or absolute path case "${configDetails}" in /*) # absolute path, no need to rewrite variable . "${configDetails}" ;; *) # relative path, prepend './' to create absolute path . "./${configDetails}" ;; esac printf "${cyan}[%s] -- [INFO] ${yellow}%s${cyan} imported --${norm}\n" \ "$(stamp)" "$configDetails" >> "$logFile" ### dump sql databases ## verify SQL variables are defined in details file # sqlServer if [ -z "${sqlServer}" ]; then badDetails empty 'sqlServer' fi # sqlUser if [ -z "${sqlUser}" ]; then badDetails empty 'sqlUser' fi # sqlPass if [ -z "${sqlPass}" ]; then badDetails empty 'sqlPass' fi ## create tmp directory and generate dumpfile names printf "${cyan}[%s] -- [INFO] Dumping SQL databases --${norm}\n" \ "$(stamp)" >> "$logFile" # create temporary directory to dump files before borg backup if ! sqlDumpDir=$( mktemp -d 2>>"$logFile" ); then exitError 111 "Could not create temporary directory to dump SQL files" fi # set cleanup flag sqlCopied=1 printf "${cyan}[%s] -- [INFO] SQL dump files will be temporarily stored in:" \ "$(stamp)" >> "$logFile" printf "\n${yellow}%s/${cyan} --${norm}\n" "$sqlDumpDir" >> "$logFile" # create unique names for database dump files sqlDump_ccnet="backup-$(date +%Y%m%d_%H%M%S)_${ccnetDB_name}.sql" sqlDump_seafile="backup-$(date +%Y%m%d_%H%M%S)_${seafileDB_name}.sql" sqlDump_seahub="backup-$(date +%Y%m%d_%H%M%S)_${seahubDB_name}.sql" ## dump databases # dump CCNET-DB if ! mysqldump -h"${sqlServer}" -u"${sqlUser}" -p"${sqlPass}" \ --opt ccnet-db > "${sqlDumpDir}/${sqlDump_ccnet}" 2>> "$logFile"; then exitError 115 "Could not dump ${ccnetDB_name} database" fi # dump SEAFILE-DB if ! mysqldump -h"${sqlServer}" -u"${sqlUser}" -p"${sqlPass}" \ --opt ccnet-db > "${sqlDumpDir}/${sqlDump_seafile}" 2>> "$logFile"; then exitError 116 "Could not dump ${seafileDB_name} database" fi # dump CCNET-DB if ! mysqldump -h"${sqlServer}" -u"${sqlUser}" -p"${sqlPass}" \ --opt ccnet-db > "${sqlDumpDir}/${sqlDump_seahub}" 2>> "$logFile"; then exitError 117 "Could not dump ${seahubDB_name} database" fi printf "${ok}[%s] -- [SUCCESS] SQL databases dumped successfully --${norm}\n" \ "$(stamp)" >> "$logFile" ### pre-backup tasks completed -- move to borg tasks printf "${ok}[%s] -- [SUCCESS] Pre-backup tasks completed --${norm}\n" \ "$(stamp)" >> "$logFile" ### Run borg variable checks printf "${cyan}[%s] -- [INFO] Verifying supplied borg details --${norm}\n" \ "$(stamp)" >> "$logFile" ## verify borg base directory if [ -z "${borgBaseDir}" ]; then badDetails empty 'borgBaseDir' elif [ ! -d "${borgBaseDir}" ]; then badDetails dne 'borgBaseDir' fi printf "${magenta}details:borgBaseDir ${norm}-- ${ok}[OK]${norm}\n" \ >> "$logFile" export BORG_BASE_DIR="${borgBaseDir%/}" ## check path to SSH keyfile if [ -z "${borgSSHKey}" ]; then badDetails empty 'borgSSHKey' elif [ ! -d "${borgSSHKey}" ]; then badDetails dne 'borgSSHKey' fi printf "${magenta}details:borgSSHKey ${norm}-- ${ok}[OK]${norm}\n" \ >> "$logFile" export BORG_RSH="ssh -i ${borgSSHKey}" ## check borg repo connect string if [ -z "${borgConnectRepo}" ]; then badDetails empty 'borgConnectRepo' fi printf "${magenta}details:borgConnectRepo ${norm}-- ${ok}[OK]${norm}\n" \ >> "$logFile" export BORG_REPO="${borgConnectRepo}" ## check borg repo password if [ -n "${borgRepoPassphrase}" ]; then printf "${magenta}details:borgRepoPassphrase ${norm}-- ${ok}[OK]${norm}\n" \ >> "$logFile" export BORG_PASSPHRASE="${borgRepoPassphrase}" else # if passwd is blank intentionally, this is insecure printf "${warn} -- [WARNING] Using a borg repo without a password is an " \ >> "$logFile" printf "insecure configuration --${norm}\n" >> "$logFile" warnCount=$((warnCount+1)) # if this was an accident, we need to provide a bogus passwd so borg fails # otherwise it will sit forever just waiting for input export BORG_PASSPHRASE="DummyPasswordSoBorgFails" fi ## export borg remote path, if specified if [ -n "${borgRemote}" ]; then export BORG_REMOTE_PATH="${borgRemote}"; fi ## read additional files if [ -n "${borgXtraListPath}" ]; then # check if file actually exists if [ ! -f "${borgXtraListPath}" ]; then badDetails dne 'borgXtraListPath' fi # read file contents into concatenated list for echo to cmdline while read -r xtraItem; do if [ -z "${xtraList}" ]; then xtraList="${xtraItem}" else xtraList="${xtraList} ${xtraItem}" fi done <> "$logFile" includeXtra=1 fi ## check if exlusion list file is specified if [ -n "${borgExcludeListPath}" ]; then # check if the file actually exists if [ ! -f "${borgExcludeListPath}" ]; then badDetails dne 'borgExcludeListPath' fi exclusions=1 fi ### create borg temp dir: ## python requires a writable temporary directory when unpacking borg and ## executing commands. This defaults to /tmp but many systems mount /tmp with ## the 'noexec' option for security. Thus, we will use/create a 'tmp' folder ## within the BORG_BASE_DIR and instruct python to use that instead of /tmp # check if BORG_BASE_DIR/tmp exists, if not, create it if [ ! -d "${borgBaseDir}/tmp" ]; then if ! mkdir "${borgBaseDir}/tmp"; then exitError 132 "Unable to create borg ${borgBaseDir}/tmp directory" else printf "${cyan}[%s] -- [INFO] Created ${yellow}%s/tmp " \ "$(stamp)" "${borgBaseDir}" >> "$logFile" printf "${cyan}--${norm}\n" >> "$logFile" fi fi export TMPDIR="${borgBaseDir}/tmp" ### execute borg depending on whether extra files and/or exclusions are defined ## construct the proper borg commandline # base command if [ "$exclusions" -eq 0 ]; then borgCMD="borg --show-rc create ${borgCreateParams} \ ::$(date +%Y-%m-%d_%H%M%S) \ ${seafDir} \ ${seafData} \ ${sqlDumpDir}" elif [ "$exclusions" -eq 1 ]; then borgCMD="borg --show-rc create ${borgCreateParams} \ --exclude-from ${borgExcludeListPath} \ ::$(date +%Y-%m-%d_%H%M%S) \ ${seafDir} \ ${seafData} \ ${sqlDumpDir}" fi # add extra locations if defined if [ "$includeXtra" -eq 1 ]; then borgCMD="${borgCMD} ${xtraList}" fi # execute borg printf "${cyan}[%s] -- [INFO] Executing borg backup operation --${norm}\n" \ "$(stamp)" >> "$logFile" ${borgCMD} 2>> "$logFile" borgResult="$?" ## check borg exit status if [ "$borgResult" -eq 0 ]; then printf "${ok}[%s] -- [SUCCESS] Borg backup completed " \ "$(stamp)" >> "$logFile" printf "successfully --${norm}\n" >> "$logFile" elif [ "$borgResult" -eq 1 ]; then printf "${warn}[%s] -- [WARNING] Borg completed with warnings. " \ "$(stamp)" >> "$logFile" printf "Review this logfile for details --${norm}\n" >> "$logFile" warnCount=$((warnCount+1)) elif [ "$borgResult" -ge 2 ]; then err_1="Borg exited with a critical error. Please review this log file" err_2="for details." exitError 138 "$err_1 $err_2" else printf "${warn}[%s] -- [WARNING] Borg exited with unknown return code. " \ "$(stamp)" >> "$logFile" printf "Review this logfile for details --${norm}\n" >> "$logFile" warnCount=$((warnCount+1)) fi ### execute borg prune if paramters are provided, otherwise skip with a warning if [ -n "${borgPruneSettings}" ]; then printf "${cyan}[%s] -- [INFO] Executing borg prune operation --${norm}\n" \ "$(stamp)" >> "$logFile" borg prune --show-rc -v "${borgPruneParams}" "${borgPruneSettings}" \ 2>> "$logFile" borgPruneResult="$?" else printf "${warn}[%s] -- [WARNING] No prune parameters provided. " \ "$(stamp)" >> "$logFile" printf "Your archive will continue growing with each backup --${norm}\n" \ >> "$logFile" warnCount=$((warnCount+1)) fi ## report on prune operation if executed if [ -n "${borgPruneResult}" ]; then if [ "${borgPruneResult}" -eq 0 ]; then printf "${ok}[%s] -- [SUCCESS] Borg prune completed --${norm}\n" \ "$(stamp)" >> "$logFile" elif [ "$borgPruneResult" -eq 1 ]; then printf "${warn}[%s] -- [WARNING] Borg prune completed with warnings. " \ "$(stamp)" >> "$logFile" printf "Review this logfile for details --${norm}\n" >> "$logFile" warnCount=$((warnCount+1)) elif [ "$borgPruneResult" -ge 2 ]; then err_1="Borg prune exited with a critical error. Please review this" err_2="log file for details." exitError 139 "$err_1 $err_2" else printf "${warn}[%s] -- [WARNING] Borg prune exited with an unknown " \ "$(stamp)" >> "$logFile" printf "return code. Review this logfile for details --${norm}\n" \ >> "$logFile" warnCount=$((warnCount+1)) fi fi ### all processes successfully completed, cleanup and exit gracefully # note successful completion of borg commands printf "${ok}[%s] -- [SUCCESS] Backup operations completed --${norm}\n" \ "$(stamp)" >> "$logFile" # cleanup cleanup # note complete success, tally warnings and exit printf "${ok}[%s] -- [SUCCESS] All processes completed --${norm}\n" \ "$(stamp)" >> "$logFile" printf "${magenta}[%s] --- %s execution completed ---${norm}\n" \ "$(stamp)" "$scriptName" >> "$logFile" if [ "$warnCount" -gt 0 ]; then printf "${warn}%s warnings issued!${norm}\n" "${warnCount}" >> "$logFile" else printf "${ok}0 warnings issued.${norm}\n" >> "$logFile" fi exit 0 ### error codes # 1: parameter error # 2: not run as root # 3: borg not installed # 100: could not start seafile service # 101: could not start seahub service # 102: could not stop seafile service # 103: could not stop seahub service # 111: could not create tmp dir for SQL dump files # 115: could not dump CCNET-DB # 116: could not dump SEAFILE-DB # 117: could not dump SEAHUB-DB # 130: null borg configuration variable # 131: invalid borg configuration variable # 138: borg exited with a critical error # 139: borg prune exited with a critical error