diff --git a/scripts/backup.py b/scripts/backup.py new file mode 100644 index 0000000..debe254 --- /dev/null +++ b/scripts/backup.py @@ -0,0 +1,126 @@ +#!/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() \ No newline at end of file