#!/bin/bash # To be run by user mkt to restore the EspoCRM pod from a backup made by # backup_espocrm.sh (a MariaDB dump + a tarball of the EspoCRM data dir). # Counterpart of backup_espocrm.sh. DANGEROUS: overwrites the current DB and # the current EspoCRM data dir with the backed-up state. # # Usage: # restore_espocrm.sh [TIMESTAMP] # restore a given set (default: latest) # restore_espocrm.sh --list # list available backup sets # restore_espocrm.sh --yes [TS] # skip the confirmation prompt # # A backup set is the pair of files # $BAK_DIR/espocrm-db-.sql.gz and $BAK_DIR/espocrm-files-.tar.gz # Environment variables POD_NAME='espocrm_pod' DB_CTR='mariadb_ctr' WEB_CTR='espocrm_ctr' DAEMON_CTR='espocrm_daemon_ctr' ESPO_IMAGE='docker.io/espocrm/espocrm:9.3.8' HOST_LOCAL_IP='127.0.0.1' WEB_HOST_PORT='8093' SITE_URL="http://${HOST_LOCAL_IP}:${WEB_HOST_PORT}" BIND_DIR="$HOME/.local/share/$POD_NAME" ESPO_DATA_DIR="$BIND_DIR/espocrm" # -> /var/www/html DB_NAME='espocrm' BAK_DIR="$HOME/bak" # systemd --user service names POD_SVC="pod-${POD_NAME}.service" DB_SVC="container-${DB_CTR}.service" WEB_SVC="container-${WEB_CTR}.service" DAEMON_SVC="container-${DAEMON_CTR}.service" # List available backup sets (timestamps that have BOTH a db and a files file) list_sets() { local f ts for f in "$BAK_DIR"/espocrm-db-*.sql.gz; do [ -e "$f" ] || continue ts=${f##*/espocrm-db-}; ts=${ts%.sql.gz} [ -e "$BAK_DIR/espocrm-files-$ts.tar.gz" ] && echo "$ts" done | sort } # Parse arguments ASSUME_YES=0 TS='' for arg in "$@"; do case "$arg" in -l|--list) echo "Available backup sets in $BAK_DIR:" list_sets | sed 's/^/ /' exit 0 ;; -y|--yes) ASSUME_YES=1 ;; -*) echo "Unknown option: $arg" >&2; exit 2 ;; *) TS="$arg" ;; esac done # Resolve the timestamp (default: latest set) if [ -z "$TS" ]; then TS=$(list_sets | tail -n 1) if [ -z "$TS" ]; then echo "ERROR: no backup sets found in $BAK_DIR." >&2 echo "Run backup_espocrm.sh first." >&2 exit 1 fi echo "No timestamp given; using latest set: $TS" fi DB_BAK="$BAK_DIR/espocrm-db-$TS.sql.gz" FILES_BAK="$BAK_DIR/espocrm-files-$TS.tar.gz" # Verify the chosen set exists and is intact BEFORE touching the live system for f in "$DB_BAK" "$FILES_BAK"; do if [ ! -f "$f" ]; then echo "ERROR: backup file not found: $f" >&2 echo "Available sets:" >&2; list_sets | sed 's/^/ /' >&2 exit 1 fi done gzip -t "$DB_BAK" || { echo "ERROR: $DB_BAK is corrupt (gzip -t failed)." >&2; exit 1; } gzip -t "$FILES_BAK" || { echo "ERROR: $FILES_BAK is corrupt (gzip -t failed)." >&2; exit 1; } echo "Backup set '$TS' found and verified:" echo " $DB_BAK" echo " $FILES_BAK" # Confirmation (overwrites current data) if [ "$ASSUME_YES" -ne 1 ]; then read -r -p "WARNING: this OVERWRITES the current DB '$DB_NAME' and ALL files under $ESPO_DATA_DIR with backup '$TS'. To confirm, type 'RESTORE espocrm_pod': " C [ "$C" = "RESTORE espocrm_pod" ] || { echo "Aborted."; exit 1; } fi # Make sure the pod and the DB are up (DB is needed for the import). systemctl --user start "$POD_SVC" "$DB_SVC" echo "Ensured pod and DB are running (rc=$?)" echo "Waiting for MariaDB to be ready..." for i in $(seq 1 60); do podman exec "$DB_CTR" mariadb-admin ping --silent >/dev/null 2>&1 && break sleep 2 done echo "MariaDB ping ready (rc=$?)" # Stop the web and daemon containers so nothing uses the data dir / DB mid-restore. systemctl --user stop "$DAEMON_SVC" "$WEB_SVC" echo "Stopped $WEB_SVC and $DAEMON_SVC (rc=$?)" # --- Restore the EspoCRM data dir --------------------------------------------- # Clear the data dir and extract the tarball INSIDE a container that mounts it, # so the bind-mount ownership is handled inside the user namespace. Use # --no-same-owner so files land owned by the container root (-> host user mkt), # matching the official image's code layout. Then chown the writable resources # (data, custom, client/custom; config.php lives under data) to www-data, so the # app can write its cache/logs/config. The entrypoint does NOT re-chown existing # files, so we do it here explicitly. echo "Restoring EspoCRM files from $FILES_BAK ..." podman run --rm -i --entrypoint sh \ -v "$ESPO_DATA_DIR":/var/www/html \ "$ESPO_IMAGE" -c ' set -e find /var/www/html -mindepth 1 -delete tar -xzf - --no-same-owner -C /var/www/html for d in data custom client/custom; do [ -e "/var/www/html/$d" ] && chown -R www-data:www-data "/var/www/html/$d" done ' < "$FILES_BAK" echo "Restored EspoCRM files (rc=$?)" # --- Restore the database ----------------------------------------------------- # The dump is a single-database dump (no CREATE DATABASE), so drop & recreate the # database with the same charset/collation, then import. Database-level grants # for the espocrm user survive a DROP DATABASE, so the app keeps its access. echo "Recreating database '$DB_NAME' ..." podman exec "$DB_CTR" sh -c \ 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" -e "DROP DATABASE IF EXISTS '"$DB_NAME"'; CREATE DATABASE '"$DB_NAME"' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"' echo "Recreated database (rc=$?)" echo "Importing DB dump from $DB_BAK ..." gunzip -c "$DB_BAK" | podman exec -i "$DB_CTR" sh -c \ 'exec mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" '"$DB_NAME" echo "Imported DB dump (rc=$?)" # --- Bring the app back up ---------------------------------------------------- # Start the web container first; its entrypoint fixes ownership of the writable # resources before the daemon (which shares the same data dir) starts. systemctl --user start "$WEB_SVC" echo "Started $WEB_SVC (rc=$?)" echo "Waiting for EspoCRM to respond..." for i in $(seq 1 90); do code=$(curl -s -o /dev/null -w '%{http_code}' "${SITE_URL}/" 2>/dev/null) case "$code" in 200|302) break ;; esac sleep 2 done echo "EspoCRM responded with HTTP $code (rc=$?)" systemctl --user start "$DAEMON_SVC" echo "Started $DAEMON_SVC (rc=$?)" # Clear cache / rebuild so the restored config and metadata take effect. podman exec -u www-data "$WEB_CTR" php command.php rebuild echo "Rebuilt EspoCRM cache (rc=$?)" echo "" echo "Restore of set '$TS' complete." echo "EspoCRM is reachable at ${SITE_URL}" echo "Status: systemctl --user status $POD_SVC $DB_SVC $WEB_SVC $DAEMON_SVC"