restoreLabel="[Restore]" # Restores site files from an archive. # # The archive is first extracted into a temporary directory located on the same # filesystem as the target vhost directory. After successful extraction, the # current vhost directory is moved to a temporary backup path and the restored # directory is moved into place. # # $1 (domain): Site domain name. # $2 (file): Source archive file path. # # Returns: # 0 on success, 1 on validation or restore failure. function restoreSiteFiles() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local target="$vhostsPath/$domain" local source="$2" if [[ -z "$source" ]]; then appError "Source files not specified" return 1 fi if [[ ! -e "$source" ]]; then appError "Source files not found: $source" return 1 fi local tmpDir restoreSource output rc if [[ -f "$source" ]]; then tmpDir=$(mktemp -d) || { appError "Failed to create temporary restore directory" return 1 } archiveExtract "$source" "$tmpDir" || { rm -rf -- "$tmpDir" &>/dev/null return 1 } restoreSource="$tmpDir" else restoreSource="$source" fi rm -rf -- "$target" || { [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to remove target directory: $target" return 1 } mkdir -p -- "$target" || { [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to create target directory: $target" return 1 } output=$(cp -a -- "$restoreSource"/. "$target/" 2>&1) rc=$? if [[ $rc -ne 0 ]]; then [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to copy restored files: $output" return 1 fi [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null openlitespeedVhostRebuild "$domain" || return 1 return 0 } # Restores a site database from a SQL file or archive. # # If the source file is an archive, it is extracted into a temporary directory # first and must contain exactly one SQL file. If the target database already # exists, the SQL file is first imported into a temporary database to validate # the restore before recreating the target database. # # $1 (domain): Site domain name. # $2 (file): Source SQL file or archive file path. # # Returns: # 0 on success, 1 on validation or restore failure. function restoreSiteDatabase() { local domain domain=$(domainPrepare "$1") domainCheck "$domain" || return 1 local file="$2" if [[ -z "$file" ]]; then appError "Source database file not specified" return 1 fi if [[ ! -f "$file" ]]; then appError "Source database file not found: $file" return 1 fi local tmpDir importFile importFile="$file" case "$file" in *.zip|*.tar.gz|*.tgz) tmpDir=$(mktemp -d) || { appError "Failed to create temporary database restore directory" return 1 } archiveExtract "$file" "$tmpDir" || { rm -rf -- "$tmpDir" &>/dev/null return 1 } local sqlCount sqlCount=$(find -- "$tmpDir" -type f -name "*.sql" | wc -l) if [[ "$sqlCount" -ne 1 ]]; then rm -rf -- "$tmpDir" &>/dev/null appError "Archive must contain exactly one SQL file: $file" return 1 fi importFile=$(find -- "$tmpDir" -type f -name "*.sql" -print -quit) ;; esac local databaseName databaseUser databasePass databaseName=$(siteConfigGetOrSet "$domain" "databaseName" "$(mariadbDomain2id "$domain")") databaseUser=$(siteConfigGetOrSet "$domain" "databaseUser" "$(mariadbDomain2id "$domain")") databasePass=$(siteConfigGetOrCreate "$domain" "databasePass") local dbExists=0 if mariadbDatabaseExists "$databaseName"; then dbExists=1 fi local output rc if (( dbExists == 0 )); then if ! mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to create database/user" return 1 fi output=$(mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$importFile" 2>&1) rc=$? if [[ $rc -ne 0 ]]; then [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "mariadbMasterImport: $output" return 1 fi [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null return 0 fi local ts tmpDb ts=$(date +%Y%m%d_%H%M%S) tmpDb="_test_${databaseName}_$ts" if ! mariadbDatabaseUserSet "$tmpDb" "$databaseUser" "$databasePass"; then [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to prepare tmp database" return 1 fi output=$(mariadbMasterImport "$tmpDb" "$databaseUser" "$databasePass" "$importFile" 2>&1) rc=$? if [[ $rc -ne 0 ]]; then mariadbDatabaseRemove "$tmpDb" [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to import tmp database: $output" return 1 fi if ! mariadbDatabaseRemove "$databaseName"; then mariadbDatabaseRemove "$tmpDb" [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to recreate target database: remove failed" return 1 fi if ! mariadbDatabaseUserSet "$databaseName" "$databaseUser" "$databasePass"; then mariadbDatabaseRemove "$tmpDb" [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to recreate target database: create failed" return 1 fi output=$(mariadbMasterImport "$databaseName" "$databaseUser" "$databasePass" "$importFile" 2>&1) rc=$? if [[ $rc -ne 0 ]]; then mariadbDatabaseRemove "$tmpDb" [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null appError "Failed to import database: $output" return 1 fi mariadbDatabaseRemove "$tmpDb" [[ -n "$tmpDir" ]] && rm -rf -- "$tmpDir" &>/dev/null return 0 } # Restores selected parts of a site from explicit source paths. # # Each source is optional. If a source path is provided, the corresponding part # of the site is restored. If a source path is empty, that part is skipped. # # $1 (domain): Site domain name. # $2 (sourceFiles): Optional source directory or archive file path with site files. # $3 (sourceDatabase): Optional source SQL file or archive file path with database dump. # $4 (sourceConf): Optional source OpenLiteSpeed virtual host config file path. # # Returns: # 0 on success, 1 on validation or restore failure. function restoreSite() { local domain error domain=$(domainPrepare "$1") if ! runError error domainCheck "$domain"; then printDanger "$siteLabel $error" return 1 fi local sourceFiles="$2" local sourceDatabase="$3" local sourceConf="$4" # Files printInfo "$restoreLabel $domain | Files: $sourceFiles" if [[ -n "$sourceFiles" ]]; then if ! runError error restoreSiteFiles "$domain" "$sourceFiles"; then printDanger "$restoreLabel $domain | Files: $error" return 1 fi printSuccess "$restoreLabel $domain | Files: Restoration complete" else printWarning "$restoreLabel $domain | Files: Skip restoring" fi # Database printInfo "$restoreLabel $domain | Database: $sourceDatabase" if [[ -n "$sourceDatabase" ]]; then if ! runError error restoreSiteDatabase "$domain" "$sourceDatabase"; then printDanger "$restoreLabel $domain | Database: $error" return 1 fi printSuccess "$restoreLabel $domain | Database: Restoration complete" else printWarning "$restoreLabel $domain | Database: Skip restoring" fi # Config printInfo "$restoreLabel $domain | Config: $sourceConf" if [[ -n "$sourceConf" ]]; then if ! runError error cp -f -- "$sourceConf" "$openlitespeedVhostsPath/$domain.conf"; then printDanger "$restoreLabel $domain | Config: $error" return 1 fi printSuccess "$restoreLabel $domain | Config: Restoration complete" else printWarning "$restoreLabel $domain | Config: Skip restoring" fi return 0 } # Restores a site from available backup entries. # # The function searches short-term and long-term backup paths for files, # database dumps, and OpenLiteSpeed virtual host configuration files matching # the specified domain. It can list available backups or restore a selected one. # # Supported index values: # empty: Show available backups and ask for a backup number. # list: Print available backup paths. # last: Restore the latest available backup. # full: Restore the latest backup that contains files, database, and config. # auto: Restore the latest full backup if available, otherwise the latest backup. # N: Restore backup by numeric index. # # $1 (domain): Site domain name. # $2 (index): Optional backup selector: list, last, full, auto, or numeric index. # # Returns: # 0 on success, 1 on validation, selection, or restore failure. function restoreRun() { local domain error domain=$(domainPrepare "$1") if ! runError error domainCheck "$domain"; then printDanger "$siteLabel $error" return 1 fi local index="$2" if [[ -n "$index" && ! "$index" =~ ^[0-9]+$ && "$index" != "auto" && "$index" != "last" && "$index" != "full" && "$index" != "list" ]]; then printDanger "Unknown value of index" return 1 fi local backupEntryList=() local backupPathList=() local path shopt -s nullglob local entryList=("$backupDailyPath"/*/*/"$domain"*) shopt -u nullglob for entry in "${entryList[@]}"; do backupEntryList+=("$entry") path=2:$(dirname -- "$entry") if ! arrayContains "$path" "${backupPathList[@]}"; then backupPathList+=("$path") fi done shopt -s nullglob local entryList=("$backupArchivePath"/*/*/"$domain"*) shopt -u nullglob for entry in "${entryList[@]}"; do backupEntryList+=("$entry") path=1:$(dirname -- "$entry") if ! arrayContains "$path" "${backupPathList[@]}"; then backupPathList+=("$path") fi done local backupSortList=($(printf "%s\n" "${backupPathList[@]}" | \ sed "s/\/${backupFirstDir}$/\/./" | \ sort -r | \ sed "s/\/.$/\/${backupFirstDir}/")) local bType bPath bTime bDate sourceList indexFull suffix local counter=1 for marker in "${backupSortList[@]}"; do bType="${marker:0:1}" bPath="${marker:2}" bTime=$(basename "$bPath") bDate=$(basename -- $(dirname -- "$bPath")) sourceList=() if arrayContains "$bPath/$domain" "${backupEntryList[@]}"; then sourceList+=('files:dir') fi if arrayContains "$bPath/$domain.tar.gz" "${backupEntryList[@]}"; then sourceList+=('files:tar') fi if arrayContains "$bPath/$domain.tgz" "${backupEntryList[@]}"; then sourceList+=('files:tar') fi if arrayContains "$bPath/$domain.zip" "${backupEntryList[@]}"; then sourceList+=('files:zip') fi if arrayContains "$bPath/$domain.sql" "${backupEntryList[@]}"; then sourceList+=('db:sql') fi if arrayContains "$bPath/$domain.sql.tar.gz" "${backupEntryList[@]}"; then sourceList+=('db:tar') fi if arrayContains "$bPath/$domain.sql.tgz" "${backupEntryList[@]}"; then sourceList+=('db:tar') fi if arrayContains "$bPath/$domain.sql.zip" "${backupEntryList[@]}"; then sourceList+=('db:zip') fi if arrayContains "$bPath/$domain.conf" "${backupEntryList[@]}"; then sourceList+=('conf') fi if arrayContains "conf" "${sourceList[@]}" && \ ( arrayContains "db:sql" "${sourceList[@]}" || arrayContains "db:tar" "${sourceList[@]}" || arrayContains "db:zip" "${sourceList[@]}" ) && \ ( arrayContains "files:dir" "${sourceList[@]}" || arrayContains "files:tar" "${sourceList[@]}" || arrayContains "files:zip" "${sourceList[@]}" ); then suffix="\033[34m${sourceList[*]}\033[0m" if [[ -z "$indexFull" ]]; then indexFull="$counter" fi else suffix="${sourceList[*]}" fi if [[ "$bType" == "1" ]]; then suffix+=" | \033[33mLongTerm\033[0m" fi if [[ -z "$index" ]]; then printf "%2s: %-19s | $suffix\n" "$counter" "$bDate/$bTime" fi ((counter++)) done local indexSelect= case "$index" in list) for marker in "${backupSortList[@]}"; do printf "%s\n" "${marker:2}" done return 0 ;; last) indexSelect=1 ;; full) indexSelect="$indexFull" ;; auto) if [[ -n "$indexFull" ]]; then indexSelect="$indexFull" else indexSelect=1 fi ;; [0-9]*) indexSelect="$index" ;; *) printWarning "$domain" "Warning! The relevant data will be cleared before restoring." read -p "Specify the backup number: " indexSelect ;; esac ((indexSelect--)) 2>/dev/null if [[ "$indexSelect" -lt 0 || "$indexSelect" -ge "${#backupSortList[@]}" ]]; then printDanger "Unknown index number" return 1 fi local restorePath="${backupSortList[$indexSelect]:2}" if [[ ! -d "$restorePath" ]]; then printDanger "Path for restore not found" return 1 fi local sourceFiles sourceDatabase sourceConf if [[ -d "$restorePath/$domain" ]]; then sourceFiles="$restorePath/$domain" elif [[ -f "$restorePath/$domain.tar.gz" ]]; then sourceFiles="$restorePath/$domain.tar.gz" elif [[ -f "$restorePath/$domain.tgz" ]]; then sourceFiles="$restorePath/$domain.tgz" elif [[ -f "$restorePath/$domain.zip" ]]; then sourceFiles="$restorePath/$domain.zip" fi if [[ -f "$restorePath/$domain.sql" ]]; then sourceDatabase="$restorePath/$domain.sql" elif [[ -f "$restorePath/$domain.sql.tar.gz" ]]; then sourceDatabase="$restorePath/$domain.sql.tar.gz" elif [[ -f "$restorePath/$domain.sql.tgz" ]]; then sourceDatabase="$restorePath/$domain.sql.tgz" elif [[ -f "$restorePath/$domain.sql.zip" ]]; then sourceDatabase="$restorePath/$domain.sql.zip" fi if [[ -f "$restorePath/$domain.conf" ]]; then sourceConf="$restorePath/$domain.conf" fi restoreSite "$domain" "$sourceFiles" "$sourceDatabase" "$sourceConf" # Clean Redis redisDomainClean "$domain" # Rebuild config site cmdSiteConfigRebuild "$domain" # Rebuild config Wordpress wordpressConfigRebuild "$domain" return 0 }