Fix backup pg_dump resolution across environments
This commit is contained in:
parent
5ee962fdb3
commit
2cef28ff3b
@ -8,6 +8,7 @@ import logging
|
||||
import hashlib
|
||||
import tarfile
|
||||
import subprocess
|
||||
import shutil
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
@ -52,6 +53,29 @@ class BackupService:
|
||||
self.db_dir.mkdir(exist_ok=True)
|
||||
self.files_dir.mkdir(exist_ok=True)
|
||||
|
||||
def _resolve_pg_binary(self, binary_name: str) -> str:
|
||||
"""Resolve PostgreSQL CLI binaries across container/host environments."""
|
||||
found = shutil.which(binary_name)
|
||||
if found:
|
||||
return found
|
||||
|
||||
candidates = [
|
||||
f"/usr/bin/{binary_name}",
|
||||
f"/usr/local/bin/{binary_name}",
|
||||
f"/opt/homebrew/bin/{binary_name}",
|
||||
f"/usr/lib/postgresql/16/bin/{binary_name}",
|
||||
f"/usr/lib/postgresql/15/bin/{binary_name}",
|
||||
f"/usr/lib/postgresql/14/bin/{binary_name}",
|
||||
]
|
||||
|
||||
for candidate in candidates:
|
||||
if Path(candidate).exists():
|
||||
return candidate
|
||||
|
||||
raise FileNotFoundError(
|
||||
f"{binary_name} blev ikke fundet i PATH. Installer postgresql-client eller rebuild API image."
|
||||
)
|
||||
|
||||
async def create_database_backup(self, is_monthly: bool = False) -> Optional[int]:
|
||||
"""
|
||||
Create PostgreSQL database backup using pg_dump
|
||||
@ -83,6 +107,8 @@ class BackupService:
|
||||
job_id, backup_format, is_monthly)
|
||||
|
||||
try:
|
||||
pg_dump_bin = self._resolve_pg_binary('pg_dump')
|
||||
|
||||
# Build pg_dump command - connect via network to postgres service
|
||||
env = os.environ.copy()
|
||||
env['PGPASSWORD'] = settings.DATABASE_URL.split(':')[2].split('@')[0] # Extract password
|
||||
@ -102,10 +128,10 @@ class BackupService:
|
||||
|
||||
if backup_format == 'dump':
|
||||
# Compressed custom format (-Fc)
|
||||
cmd = ['pg_dump', '-h', host, '-U', user, '-Fc', dbname]
|
||||
cmd = [pg_dump_bin, '-h', host, '-U', user, '-Fc', dbname]
|
||||
else:
|
||||
# Plain SQL format
|
||||
cmd = ['pg_dump', '-h', host, '-U', user, dbname]
|
||||
cmd = [pg_dump_bin, '-h', host, '-U', user, dbname]
|
||||
|
||||
# Execute pg_dump and write to file
|
||||
logger.info("📦 Executing: %s > %s", ' '.join(cmd), backup_path)
|
||||
@ -154,6 +180,21 @@ class BackupService:
|
||||
backup_path.unlink()
|
||||
|
||||
return None
|
||||
except FileNotFoundError as e:
|
||||
error_msg = str(e)
|
||||
logger.error("❌ Database backup failed: %s", error_msg)
|
||||
|
||||
execute_update(
|
||||
"""UPDATE backup_jobs
|
||||
SET status = %s, completed_at = %s, error_message = %s
|
||||
WHERE id = %s""",
|
||||
('failed', datetime.now(), error_msg, job_id)
|
||||
)
|
||||
|
||||
if backup_path.exists():
|
||||
backup_path.unlink()
|
||||
|
||||
return None
|
||||
|
||||
async def create_files_backup(self) -> Optional[int]:
|
||||
"""
|
||||
@ -412,9 +453,12 @@ class BackupService:
|
||||
|
||||
env['PGPASSWORD'] = password
|
||||
|
||||
psql_bin = self._resolve_pg_binary('psql')
|
||||
pg_restore_bin = self._resolve_pg_binary('pg_restore')
|
||||
|
||||
# Step 1: Create new empty database
|
||||
logger.info("📦 Creating new database: %s", new_dbname)
|
||||
create_cmd = ['psql', '-h', host, '-U', user, '-d', 'postgres', '-c',
|
||||
create_cmd = [psql_bin, '-h', host, '-U', user, '-d', 'postgres', '-c',
|
||||
f"CREATE DATABASE {new_dbname} OWNER {user};"]
|
||||
result = subprocess.run(create_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
text=True, env=env)
|
||||
@ -430,7 +474,7 @@ class BackupService:
|
||||
# Build restore command based on format
|
||||
if backup['backup_format'] == 'dump':
|
||||
# Restore from compressed custom format
|
||||
cmd = ['pg_restore', '-h', host, '-U', user, '-d', new_dbname]
|
||||
cmd = [pg_restore_bin, '-h', host, '-U', user, '-d', new_dbname]
|
||||
|
||||
logger.info("📥 Restoring to %s: %s < %s", new_dbname, ' '.join(cmd), backup_path)
|
||||
|
||||
@ -461,7 +505,7 @@ class BackupService:
|
||||
if has_real_errors and not is_harmless:
|
||||
logger.error("❌ pg_restore had REAL errors: %s", result.stderr[:1000])
|
||||
# Try to drop the failed database
|
||||
subprocess.run(['psql', '-h', host, '-U', user, '-d', 'postgres', '-c',
|
||||
subprocess.run([psql_bin, '-h', host, '-U', user, '-d', 'postgres', '-c',
|
||||
f"DROP DATABASE IF EXISTS {new_dbname};"], env=env)
|
||||
raise RuntimeError(f"pg_restore failed with errors")
|
||||
else:
|
||||
@ -469,7 +513,7 @@ class BackupService:
|
||||
|
||||
else:
|
||||
# Restore from plain SQL
|
||||
cmd = ['psql', '-h', host, '-U', user, '-d', new_dbname]
|
||||
cmd = [psql_bin, '-h', host, '-U', user, '-d', new_dbname]
|
||||
|
||||
logger.info("📥 Executing: %s < %s", ' '.join(cmd), backup_path)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user