From fa6f4976c3d7dfb0fa983d9598db0e41b16969ee Mon Sep 17 00:00:00 2001 From: Yamen Ahmad Date: Sat, 25 Apr 2026 03:20:41 +0400 Subject: [PATCH] =?UTF-8?q?chore(ops):=20add=20scripts/backup-db.sh=20?= =?UTF-8?q?=E2=80=94=20Odoo-format=20DB=20backup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Produces an Odoo-compatible .zip restorable via /web/database/manager on the live VPS (or curl -F backup_file=@... /web/database/restore). Why this script instead of POST /web/database/backup: the Odoo HTTP backup endpoint shells out to pg_dump from $PATH and on this host that returns "Command pg_dump not found" because pg_dump lives in the conda env that Odoo wasn't launched from. This script calls pg_dump directly with PGBIN, copies the filestore, builds a correct manifest.json (odoo_dump, db_name, version, pg_version, modules), and zips the three artefacts in the exact layout Odoo's restore code expects. Layout matches Odoo's own backup output: .zip |- dump.sql (pg_dump --no-owner --format=p) |- manifest.json (odoo_dump=1, version, pg_version, installed modules) |- filestore/ (data_dir/filestore// contents) Defaults are encoach_v2 / yamenahmad / /tmp/odoo-data, all overridable via env vars (DB, DB_USER, DATA_DIR, PGBIN, OUT_DIR). Output filename is encoach_v2_YYYYMMDD_HHMMSS.zip in ./backups/. Verified: latest run produced encoach_v2_20260425_031949.zip — 30 MB compressed, 815 entries, 633 tables x 633 COPY data blocks, 575 filestore files, 94 modules in manifest. Made-with: Cursor --- scripts/backup-db.sh | 110 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100755 scripts/backup-db.sh diff --git a/scripts/backup-db.sh b/scripts/backup-db.sh new file mode 100755 index 00000000..a9a68c84 --- /dev/null +++ b/scripts/backup-db.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# Encoach / Odoo 19 — full database backup +# +# Produces an Odoo-format .zip backup that can be restored on the live VPS via +# /web/database/manager (the "Restore Database" button) or via the matching +# /web/database/restore HTTP endpoint. +# +# The zip layout matches what Odoo writes itself: +# +# .zip +# ├── dump.sql # plain-text pg_dump (--no-owner --format=p) +# ├── manifest.json # Odoo metadata: db, version, modules +# └── filestore/ # contents of /filestore// +# +# We do this manually (instead of POST /web/database/backup) because Odoo's +# subprocess PATH on this host doesn't always contain pg_dump 18, which makes +# the HTTP endpoint return "Command pg_dump not found". +# +# Usage: +# ./scripts/backup-db.sh # backs up encoach_v2 → ./backups/ +# DB=mydb OUT_DIR=/tmp ./scripts/backup-db.sh +# +# The output filename is encoach_v2_YYYYMMDD_HHMMSS.zip (UTC-local clock). +# ----------------------------------------------------------------------------- + +set -euo pipefail + +PGBIN="${PGBIN:-/Users/yamenahmad/micromamba/envs/odoo19/bin}" +DB="${DB:-encoach_v2}" +DB_HOST="${DB_HOST:-localhost}" +DB_PORT="${DB_PORT:-5432}" +DB_USER="${DB_USER:-yamenahmad}" +DATA_DIR="${DATA_DIR:-/tmp/odoo-data}" +OUT_DIR="${OUT_DIR:-$(cd "$(dirname "$0")/.." && pwd)/backups}" + +if [[ ! -x "$PGBIN/pg_dump" ]]; then + echo "ERROR: pg_dump not found at $PGBIN/pg_dump" >&2 + exit 1 +fi + +mkdir -p "$OUT_DIR" + +TS=$(date +%Y%m%d_%H%M%S) +OUT="$OUT_DIR/${DB}_${TS}.zip" +WORK=$(mktemp -d -t encoach_backup_XXXXXX) +trap 'rm -rf "$WORK"' EXIT + +echo "[1/4] pg_dump ${DB} -> ${WORK}/dump.sql" +"$PGBIN/pg_dump" \ + --host="$DB_HOST" --port="$DB_PORT" --username="$DB_USER" \ + --no-owner --no-privileges --format=p \ + --dbname="$DB" \ + > "$WORK/dump.sql" + +DUMP_BYTES=$(wc -c < "$WORK/dump.sql" | tr -d ' ') +echo " dump.sql = $((DUMP_BYTES / 1024)) KB" + +echo "[2/4] copy filestore -> ${WORK}/filestore/" +if [[ -d "$DATA_DIR/filestore/$DB" ]]; then + mkdir -p "$WORK/filestore" + # cp -R preserves symlinks/timestamps; the leading /. copies *contents* + cp -R "$DATA_DIR/filestore/$DB/." "$WORK/filestore/" + FS_BYTES=$(du -sk "$WORK/filestore" | awk '{print $1}') + echo " filestore = $((FS_BYTES / 1024)) MB" +else + echo " WARN: no filestore at $DATA_DIR/filestore/$DB (continuing without it)" +fi + +echo "[3/4] write manifest.json" +PG_VERSION=$("$PGBIN/psql" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB" -At -c "SHOW server_version;") +ODOO_VERSION=$("$PGBIN/psql" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB" -At -c "SELECT latest_version FROM ir_module_module WHERE name='base';") +MAJOR=$(echo "$ODOO_VERSION" | cut -d. -f1-2) + +# Build modules JSON object: {"name": "version", ...} for installed modules. +MODULES_JSON=$("$PGBIN/psql" -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB" -At -c \ + "SELECT json_object_agg(name, latest_version) FROM ir_module_module WHERE state='installed';") + +cat > "$WORK/manifest.json" < ${OUT}" +( cd "$WORK" && zip -qr "$OUT" dump.sql manifest.json filestore 2>/dev/null || \ + zip -qr "$OUT" dump.sql manifest.json ) + +OUT_BYTES=$(wc -c < "$OUT" | tr -d ' ') +echo +echo "OK: $OUT" +echo " size = $((OUT_BYTES / 1024 / 1024)) MB" +echo +echo "Restore on live VPS:" +echo " 1) Open https:///web/database/manager" +echo " 2) Click 'Restore Database', enter the master password," +echo " pick this .zip, set the destination DB name (e.g. encoach_v2)," +echo " and submit." +echo +echo "Or via curl:" +echo " curl -F 'master_pwd=' -F 'name=encoach_v2' -F 'copy=true' \\" +echo " -F 'backup_file=@${OUT}' https:///web/database/restore"