#!/usr/bin/env python3 """SuperSam daily backup — export public tables to CSV, tar.gz, upload to S3.""" import csv import io import os import sys import subprocess import tarfile import tempfile from datetime import datetime import boto3 from botocore.config import Config # S3 config S3_ENDPOINT = "https://s3.ru1.storage.beget.cloud" S3_KEY = "YG4MQNKAPNL65200MBUY" S3_SECRET = "8mXkFM2VRQ3pN1Nx4mhmJ2jrZoB5YTPUa4CaZh43" S3_BUCKET = "02f162ff4a18-supersam-s3" # DB config DB_HOST = "supabase-db" DB_USER = "supabase_admin" DB_NAME = "postgres" def get_tables(): """Get list of public tables with data.""" result = subprocess.run( ["docker", "exec", "-i", DB_HOST, "psql", "-U", DB_USER, "-d", DB_NAME, "-t", "-A", "-c", "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename;"], capture_output=True, text=True ) if result.returncode != 0: print(f"ERROR getting tables: {result.stderr}", file=sys.stderr) sys.exit(1) tables = [t.strip() for t in result.stdout.strip().split("\n") if t.strip()] return tables def export_table_csv(table_name, out_dir): """Export a single table to CSV via psql \\copy.""" csv_path = os.path.join(out_dir, f"{table_name}.csv") # Use psql \copy (client-side) to avoid needing superuser for COPY cmd = [ "docker", "exec", "-i", DB_HOST, "psql", "-U", DB_USER, "-d", DB_NAME, "-c", f"\\copy (SELECT * FROM public.{table_name}) TO '/tmp/{table_name}.csv' WITH CSV HEADER;" ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(f"WARN: {table_name} export failed: {result.stderr}", file=sys.stderr) return False # Copy from container to host cmd2 = ["docker", "cp", f"{DB_HOST}:/tmp/{table_name}.csv", csv_path] result2 = subprocess.run(cmd2, capture_output=True, text=True) if result2.returncode != 0: print(f"WARN: {table_name} docker cp failed: {result2.stderr}", file=sys.stderr) return False # Clean up temp file in container subprocess.run(["docker", "exec", "-i", DB_HOST, "rm", "-f", f"/tmp/{table_name}.csv"], capture_output=True, text=True) # Check if file has data (more than just header) with open(csv_path, "r") as f: lines = f.readlines() if len(lines) <= 1: print(f" {table_name}: empty (skipping)") return False print(f" {table_name}: {len(lines)-1} rows") return True def upload_to_s3(file_path, key): """Upload file to S3.""" s3 = boto3.client( "s3", endpoint_url=S3_ENDPOINT, aws_access_key_id=S3_KEY, aws_secret_access_key=S3_SECRET, config=Config(signature_version="s3v4"), region_name="ru-1" ) s3.upload_file(file_path, S3_BUCKET, key) print(f" Uploaded: s3://{S3_BUCKET}/{key}") def main(): date_str = datetime.now().strftime("%Y-%m-%d") archive_name = f"supersam-backup-{date_str}.tar.gz" s3_key = f"backups/{archive_name}" print(f"=== SuperSam Backup {date_str} ===") with tempfile.TemporaryDirectory() as tmpdir: tables = get_tables() print(f"Found {len(tables)} tables") exported = [] for table in tables: if export_table_csv(table, tmpdir): exported.append(table) if not exported: print("No tables with data — nothing to backup") sys.exit(0) # Create tar.gz archive_path = os.path.join(tmpdir, archive_name) with tarfile.open(archive_path, "w:gz") as tar: for table in exported: csv_path = os.path.join(tmpdir, f"{table}.csv") tar.add(csv_path, arcname=f"{table}.csv") # Get size size_mb = os.path.getsize(archive_path) / (1024 * 1024) print(f"Archive: {archive_name} ({size_mb:.2f} MB)") # Upload upload_to_s3(archive_path, s3_key) print(f"=== Backup complete ===") if __name__ == "__main__": main()