qwen_2.5_model / scripts /restore.sh
Muhammad Noman
Deploy OpenWA to Hugging Face Spaces
46252cd
Raw
History Blame Contribute Delete
7.75 kB
#!/usr/bin/env bash
#
# OpenWA restore.
#
# Restores the always-SQLite auth/audit database, a SQLite data store, engine authentication, local
# media, installed plugins, and bootstrap configuration from an archive produced by scripts/backup.sh.
# PostgreSQL dumps are staged for the explicit psql import printed at the end of the restore.
#
# Usage:
# ./scripts/restore.sh <backup-archive.tar.gz>
# Environment:
# OPENWA_DATA_DIR data directory to restore into (default: ./data)
# SESSION_DATA_PATH, BAILEYS_AUTH_DIR, STORAGE_LOCAL_PATH, PLUGINS_DIR
# override the corresponding state directories
#
# Stop the OpenWA app before restoring. A snapshot of the current data dir is taken
# first so a bad restore can be undone.
#
set -euo pipefail
# Restored databases, credentials, and snapshots must not inherit a permissive operator umask.
umask 077
ARCHIVE="${1:-}"
DATA_DIR="${OPENWA_DATA_DIR:-./data}"
SESSIONS_DIR="${SESSION_DATA_PATH:-$DATA_DIR/sessions}"
BAILEYS_DIR="${BAILEYS_AUTH_DIR:-$DATA_DIR/baileys}"
MEDIA_DIR="${STORAGE_LOCAL_PATH:-$DATA_DIR/media}"
PLUGIN_PACKAGES_DIR="${PLUGINS_DIR:-./plugins}"
PLUGIN_STATE_DIR="$DATA_DIR/plugins"
RESTORE_TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
RESOLVED_CWD="$(pwd -P)"
# Resolve symlinks in the nearest existing ancestor as well as lexical '..' segments. This matters for
# destructive targets such as /mount-link/sessions: path.resolve alone would not reveal that mount-link
# points at the workspace, home directory, or another broad protected target.
resolve_path() {
node -e '
const fs = require("fs");
const path = require("path");
let current = path.resolve(process.argv[1]);
const missing = [];
while (!fs.existsSync(current)) {
const parent = path.dirname(current);
if (parent === current) break;
missing.unshift(path.basename(current));
current = parent;
}
const physical = fs.existsSync(current) ? fs.realpathSync(current) : current;
console.log(path.join(physical, ...missing));
' "$1"
}
RESOLVED_DATA_DIR="$(resolve_path "$DATA_DIR")"
RESOLVED_USER_HOME="$(resolve_path "${HOME:-/nonexistent-openwa-home}")"
log() { echo "[restore] $*"; }
case "$DATA_DIR" in
'' | / | . | ./ | .. | ../)
log "ERROR: refusing unsafe OPENWA_DATA_DIR target: ${DATA_DIR:-<empty>}"
exit 1
;;
esac
case "$RESOLVED_CWD/" in
"$RESOLVED_DATA_DIR"/*)
log "ERROR: OPENWA_DATA_DIR must not be the workspace or one of its parent directories: $DATA_DIR"
exit 1
;;
esac
if [ "$RESOLVED_DATA_DIR" = "$RESOLVED_USER_HOME" ]; then
log "ERROR: OPENWA_DATA_DIR must not be the user home directory: $DATA_DIR"
exit 1
fi
replace_tree() {
source_dir="$1"
target_dir="$2"
label="$3"
case "$target_dir" in
'' | / | . | ./ | .. | ../)
log "ERROR: refusing to replace unsafe $label target: ${target_dir:-<empty>}"
exit 1
;;
esac
resolved_target="$(resolve_path "$target_dir")"
if [ "$resolved_target" = "/" ] || [ "$resolved_target" = "$RESOLVED_CWD" ] || [ "$resolved_target" = "$RESOLVED_DATA_DIR" ] || [ "$resolved_target" = "$RESOLVED_USER_HOME" ]; then
log "ERROR: refusing to replace broad $label target: $target_dir"
exit 1
fi
case "$RESOLVED_CWD/" in
"$resolved_target"/*)
log "ERROR: refusing to replace $label target that contains the workspace: $target_dir"
exit 1
;;
esac
case "$RESOLVED_DATA_DIR/" in
"$resolved_target"/*)
log "ERROR: refusing to replace $label target that contains the data directory: $target_dir"
exit 1
;;
esac
# The initial data-dir snapshot already covers normal nested targets. Preserve any custom target
# outside data/ separately before replacing it, so a custom PLUGINS_DIR/auth path remains recoverable.
case "$resolved_target" in
"$RESOLVED_DATA_DIR"/*) ;;
*)
if [ -e "$target_dir" ]; then
external_snapshot="${target_dir%/}.pre-restore-$RESTORE_TIMESTAMP"
log "Snapshotting current $label -> $external_snapshot"
cp -pR "$target_dir" "$external_snapshot"
fi
;;
esac
log "Restoring $label"
rm -rf -- "$target_dir"
mkdir -p "$(dirname "$target_dir")"
cp -pR "$source_dir" "$target_dir"
}
if [ -z "$ARCHIVE" ] || [ ! -f "$ARCHIVE" ]; then
echo "Usage: $0 <backup-archive.tar.gz>" >&2
exit 1
fi
STAGE="$(mktemp -d)"
trap 'rm -rf "$STAGE"' EXIT
log "Extracting $ARCHIVE"
# Refuse archive path names that could escape STAGE. This restore command accepts archives produced by
# backup.sh; path validation is an additional traversal guard, not a general untrusted-tar verifier.
while IFS= read -r entry; do
case "$entry" in
/* | ../* | */../* | */..)
log "ERROR: unsafe path in backup archive: $entry"
exit 1
;;
esac
done < <(tar -tzf "$ARCHIVE")
tar -xzf "$ARCHIVE" -C "$STAGE"
# Safety snapshot of whatever is there now.
if [ -d "$DATA_DIR" ] && [ -n "$(ls -A "$DATA_DIR" 2>/dev/null || true)" ]; then
SAFETY="${DATA_DIR%/}.pre-restore-$RESTORE_TIMESTAMP"
log "Snapshotting current data dir -> $SAFETY"
cp -pR "$DATA_DIR" "$SAFETY"
fi
mkdir -p "$DATA_DIR"
if [ -f "$STAGE/main.sqlite" ]; then
log "Restoring auth/audit DB (main.sqlite)"
cp "$STAGE/main.sqlite" "$DATA_DIR/main.sqlite"
else
log "WARN: main.sqlite not in archive — API keys / audit log will NOT be restored"
fi
if [ -f "$STAGE/openwa.sqlite" ]; then
log "Restoring data store (openwa.sqlite)"
cp "$STAGE/openwa.sqlite" "$DATA_DIR/openwa.sqlite"
fi
if [ -d "$STAGE/sessions" ]; then
replace_tree "$STAGE/sessions" "$SESSIONS_DIR" "whatsapp-web.js sessions"
fi
if [ -d "$STAGE/baileys" ]; then
replace_tree "$STAGE/baileys" "$BAILEYS_DIR" "Baileys authentication state"
fi
if [ -d "$STAGE/media" ]; then
replace_tree "$STAGE/media" "$MEDIA_DIR" "local media"
fi
if [ -d "$STAGE/plugin-packages" ] && [ -d "$STAGE/plugin-state" ]; then
RESOLVED_PLUGIN_PACKAGES_DIR="$(resolve_path "$PLUGIN_PACKAGES_DIR")"
RESOLVED_PLUGIN_STATE_DIR="$(resolve_path "$PLUGIN_STATE_DIR")"
if [ "$RESOLVED_PLUGIN_PACKAGES_DIR" = "$RESOLVED_PLUGIN_STATE_DIR" ]; then
# Docker deployments deliberately colocate package and state files. Build the complete target in
# staging and replace it once, so neither half can erase the other during restore.
MERGED_PLUGINS_DIR="$STAGE/plugin-merged"
mkdir -p "$MERGED_PLUGINS_DIR"
cp -pR "$STAGE/plugin-packages/." "$MERGED_PLUGINS_DIR"
cp -pR "$STAGE/plugin-state/." "$MERGED_PLUGINS_DIR"
replace_tree "$MERGED_PLUGINS_DIR" "$PLUGIN_PACKAGES_DIR" "installed plugins and plugin state"
else
replace_tree "$STAGE/plugin-packages" "$PLUGIN_PACKAGES_DIR" "installed plugin packages"
replace_tree "$STAGE/plugin-state" "$PLUGIN_STATE_DIR" "plugin registry and persisted state"
fi
elif [ -d "$STAGE/plugin-packages" ]; then
replace_tree "$STAGE/plugin-packages" "$PLUGIN_PACKAGES_DIR" "installed plugin packages"
elif [ -d "$STAGE/plugin-state" ]; then
replace_tree "$STAGE/plugin-state" "$PLUGIN_STATE_DIR" "plugin registry and persisted state"
fi
if [ -f "$STAGE/.env.generated" ]; then
log "Restoring dashboard-generated configuration"
cp "$STAGE/.env.generated" "$DATA_DIR/.env.generated"
chmod 0600 "$DATA_DIR/.env.generated"
fi
if [ -f "$STAGE/.api-key" ]; then
log "Restoring plaintext admin key"
cp "$STAGE/.api-key" "$DATA_DIR/.api-key"
chmod 0600 "$DATA_DIR/.api-key"
fi
if [ -f "$STAGE/database.sql" ]; then
cp "$STAGE/database.sql" "$DATA_DIR/database.sql"
log "Postgres dump present — import it manually into your Postgres instance:"
log " psql \"\$DATABASE_URL\" < $DATA_DIR/database.sql"
fi
log "Restore complete. Start the app and confirm an existing API key still authenticates."