#!/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) mag=$(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" 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" 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" # borg output verbosity -- normal borgCreateParams='--stats' borgPruneParams='--list' configDetails="$scriptPath/seafbackup.details" err503Copied=0 sqlCopied=0 # 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 "${mag}[%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" 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 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 "${cyan}[%s] -- [INFO] 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" # 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 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