mirror of
https://github.com/mruwnik/memory.git
synced 2026-01-02 17:22:58 +01:00
tools/restore_databases.sh: Script to restore PostgreSQL and Qdrant backups from encrypted backup files. tools/restore_files.py: Python script to restore Fernet-encrypted file backups. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
183 lines
5.3 KiB
Python
Executable File
183 lines
5.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Restore Fernet-encrypted file backups from S3.
|
|
|
|
Usage:
|
|
# List available backups
|
|
python restore_files.py --list
|
|
|
|
# Restore a specific backup
|
|
python restore_files.py emails.tar.gz.enc --output ./restored_files
|
|
|
|
# Restore from local file
|
|
python restore_files.py /path/to/backup.tar.gz.enc --output ./restored_files
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import io
|
|
import os
|
|
import sys
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
import boto3
|
|
from cryptography.fernet import Fernet
|
|
|
|
|
|
def get_cipher(password: str) -> Fernet:
|
|
"""Create Fernet cipher from password (same derivation as backup.py)."""
|
|
key_bytes = hashlib.sha256(password.encode()).digest()
|
|
key = base64.urlsafe_b64encode(key_bytes)
|
|
return Fernet(key)
|
|
|
|
|
|
def list_backups(bucket: str, prefix: str, region: str) -> list[str]:
|
|
"""List available encrypted file backups in S3."""
|
|
s3 = boto3.client("s3", region_name=region)
|
|
|
|
try:
|
|
response = s3.list_objects_v2(Bucket=bucket, Prefix=prefix)
|
|
except Exception as e:
|
|
print(f"Error listing S3 bucket: {e}", file=sys.stderr)
|
|
return []
|
|
|
|
backups = []
|
|
for obj in response.get("Contents", []):
|
|
key = obj["Key"]
|
|
if key.endswith(".tar.gz.enc"):
|
|
name = key.split("/")[-1]
|
|
size_mb = obj["Size"] / (1024 * 1024)
|
|
modified = obj["LastModified"].strftime("%Y-%m-%d %H:%M:%S")
|
|
backups.append(f"{name:40} {size_mb:8.2f} MB {modified}")
|
|
|
|
return backups
|
|
|
|
|
|
def download_from_s3(
|
|
bucket: str, prefix: str, filename: str, region: str
|
|
) -> bytes | None:
|
|
"""Download encrypted backup from S3."""
|
|
s3 = boto3.client("s3", region_name=region)
|
|
key = f"{prefix}/{filename}"
|
|
|
|
try:
|
|
print(f"Downloading s3://{bucket}/{key}...")
|
|
response = s3.get_object(Bucket=bucket, Key=key)
|
|
return response["Body"].read()
|
|
except Exception as e:
|
|
print(f"Error downloading from S3: {e}", file=sys.stderr)
|
|
return None
|
|
|
|
|
|
def decrypt_and_extract(
|
|
encrypted_data: bytes, password: str, output_dir: Path
|
|
) -> bool:
|
|
"""Decrypt Fernet-encrypted tarball and extract contents."""
|
|
cipher = get_cipher(password)
|
|
|
|
try:
|
|
print("Decrypting...")
|
|
decrypted = cipher.decrypt(encrypted_data)
|
|
except Exception as e:
|
|
print(f"Decryption failed: {e}", file=sys.stderr)
|
|
print("Check that BACKUP_ENCRYPTION_KEY is correct", file=sys.stderr)
|
|
return False
|
|
|
|
print(f"Decrypted {len(decrypted)} bytes")
|
|
|
|
try:
|
|
print(f"Extracting to {output_dir}...")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
tar_buffer = io.BytesIO(decrypted)
|
|
with tarfile.open(fileobj=tar_buffer, mode="r:gz") as tar:
|
|
tar.extractall(output_dir)
|
|
print("Extraction complete")
|
|
return True
|
|
except Exception as e:
|
|
print(f"Extraction failed: {e}", file=sys.stderr)
|
|
return False
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Restore Fernet-encrypted file backups from S3"
|
|
)
|
|
parser.add_argument(
|
|
"backup",
|
|
nargs="?",
|
|
help="Backup filename (e.g., emails.tar.gz.enc) or local path",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
"-o",
|
|
type=Path,
|
|
default=Path("./restored_files"),
|
|
help="Output directory for restored files",
|
|
)
|
|
parser.add_argument("--list", "-l", action="store_true", help="List available backups")
|
|
parser.add_argument(
|
|
"--bucket",
|
|
default=os.getenv("S3_BACKUP_BUCKET", "equistamp-memory-backup"),
|
|
help="S3 bucket name",
|
|
)
|
|
parser.add_argument(
|
|
"--prefix",
|
|
default=os.getenv("S3_BACKUP_PREFIX", "Daniel"),
|
|
help="S3 prefix",
|
|
)
|
|
parser.add_argument(
|
|
"--region",
|
|
default=os.getenv("S3_BACKUP_REGION", "eu-central-1"),
|
|
help="AWS region",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Get encryption key
|
|
password = os.getenv("BACKUP_ENCRYPTION_KEY")
|
|
if not password and not args.list:
|
|
print("Error: BACKUP_ENCRYPTION_KEY environment variable not set", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# List mode
|
|
if args.list:
|
|
print(f"Available backups in s3://{args.bucket}/{args.prefix}/:\n")
|
|
backups = list_backups(args.bucket, args.prefix, args.region)
|
|
if backups:
|
|
print("Name Size Modified")
|
|
print("-" * 70)
|
|
for backup in backups:
|
|
print(backup)
|
|
else:
|
|
print("No encrypted backups found")
|
|
return
|
|
|
|
# Restore mode
|
|
if not args.backup:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
# Check if it's a local file or S3 key
|
|
local_path = Path(args.backup)
|
|
if local_path.exists():
|
|
print(f"Reading local file: {local_path}")
|
|
encrypted_data = local_path.read_bytes()
|
|
else:
|
|
# Download from S3
|
|
encrypted_data = download_from_s3(
|
|
args.bucket, args.prefix, args.backup, args.region
|
|
)
|
|
if not encrypted_data:
|
|
sys.exit(1)
|
|
|
|
# Decrypt and extract
|
|
if decrypt_and_extract(encrypted_data, password, args.output):
|
|
print(f"\nFiles restored to: {args.output.absolute()}")
|
|
else:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|