#!/usr/bin/env nu # VAPORA Database Backup Script - SurrealDB to S3 + Restic # Follows NUSHELL_GUIDELINES.md strictly (0.109.0+) # Get ISO 8601 timestamp def get-timestamp []: nothing -> string { date now | format date "%Y%m%d-%H%M%S" } # Export SurrealDB def export-database [ surreal_url: string surreal_user: string surreal_pass: string output_file: string ]: nothing -> record { print $"Exporting database from [$surreal_url]..." let result = do { ^surreal export \ --conn $surreal_url \ --user $surreal_user \ --pass $surreal_pass \ --output $output_file } | complete if ($result.exit_code == 0) { { success: true file: $output_file timestamp: (get-timestamp) error: null } } else { { success: false file: $output_file timestamp: (get-timestamp) error: ($result.stderr | str trim) } } } # Compress backup def compress-backup [input_file: string]: nothing -> record { print $"Compressing [$input_file]..." let compressed = $"($input_file).gz" let result = do { ^gzip --force $input_file } | complete if ($result.exit_code == 0) { { success: true original: $input_file compressed: $compressed error: null } } else { { success: false original: $input_file compressed: $compressed error: ($result.stderr | str trim) } } } # Encrypt with AES-256 def encrypt-backup [ input_file: string key_file: string ]: nothing -> record { print $"Encrypting [$input_file]..." let encrypted = $"($input_file).enc" let result = do { ^openssl enc -aes-256-cbc \ -in $input_file \ -out $encrypted \ -pass file:$key_file } | complete if ($result.exit_code == 0) { { success: true encrypted_file: $encrypted error: null } } else { { success: false encrypted_file: $encrypted error: ($result.stderr | str trim) } } } # Upload to S3 def upload-to-s3 [ file_path: string s3_bucket: string s3_prefix: string ]: nothing -> record { print $"Uploading to S3 [$s3_bucket]..." let s3_key = $"($s3_prefix)/database-$(get-timestamp).sql.gz.enc" let result = do { ^aws s3 cp $file_path \ $"s3://($s3_bucket)/($s3_key)" \ --sse AES256 \ --metadata $"backup-type=database,timestamp=$(get-timestamp)" } | complete if ($result.exit_code == 0) { { success: true s3_location: $"s3://($s3_bucket)/($s3_key)" timestamp: (get-timestamp) error: null } } else { { success: false s3_location: $"s3://($s3_bucket)/($s3_key)" error: ($result.stderr | str trim) } } } # Verify S3 backup exists def verify-backup [s3_location: string]: nothing -> record { print $"Verifying backup [$s3_location]..." let result = do { ^aws s3 ls $s3_location --human-readable } | complete if ($result.exit_code == 0) { { success: true location: $s3_location size_info: ($result.stdout | str trim) error: null } } else { { success: false location: $s3_location error: ($result.stderr | str trim) } } } # Cleanup temporary files def cleanup-temp-files [temp_dir: string]: nothing -> record { print $"Cleaning up [$temp_dir]..." let result = do { ^rm -rf $temp_dir } | complete if ($result.exit_code == 0) { { success: true removed: $temp_dir error: null } } else { { success: false removed: $temp_dir error: ($result.stderr | str trim) } } } # Main backup procedure def main [ --surreal-url: string = "ws://localhost:8000" --surreal-user: string = "root" --surreal-pass: string = "" --s3-bucket: string = "" --s3-prefix: string = "backups/database" --encryption-key: string = "" --work-dir: string = "/tmp/vapora-backups" ]: nothing { print "=== VAPORA Database Backup (S3) ===" print "" if ($s3_bucket == "") { print "ERROR: --s3-bucket is required" exit 1 } if ($surreal_pass == "") { print "ERROR: --surreal-pass is required" exit 1 } if ($encryption_key == "") { print "ERROR: --encryption-key is required" exit 1 } # Create work directory let work_path = $"($work_dir)/$(get-timestamp)" let create_result = do { ^mkdir -p $work_path } | complete if (not ($create_result.exit_code == 0)) { print "ERROR: Failed to create work directory" exit 1 } # Export database let backup_file = $"($work_path)/vapora-db.sql" let export_result = (export-database $surreal_url $surreal_user $surreal_pass $backup_file) if (not $export_result.success) { print $"ERROR: Database export failed: ($export_result.error)" cleanup-temp-files $work_path exit 1 } print "✓ Database exported successfully" # Compress let compress_result = (compress-backup $backup_file) if (not $compress_result.success) { print $"ERROR: Compression failed: ($compress_result.error)" cleanup-temp-files $work_path exit 1 } print "✓ Backup compressed" # Encrypt let encrypt_result = (encrypt-backup $compress_result.compressed $encryption_key) if (not $encrypt_result.success) { print $"ERROR: Encryption failed: ($encrypt_result.error)" cleanup-temp-files $work_path exit 1 } print "✓ Backup encrypted" # Upload to S3 let upload_result = (upload-to-s3 $encrypt_result.encrypted_file $s3_bucket $s3_prefix) if (not $upload_result.success) { print $"ERROR: S3 upload failed: ($upload_result.error)" cleanup-temp-files $work_path exit 1 } print "✓ Backup uploaded to S3" # Verify let verify_result = (verify-backup $upload_result.s3_location) if (not $verify_result.success) { print $"ERROR: Backup verification failed: ($verify_result.error)" cleanup-temp-files $work_path exit 1 } print "✓ Backup verified" # Cleanup cleanup-temp-files $work_path # Summary print "" print "=== Backup Complete ===" print $"Location: [$upload_result.s3_location]" print $"Size: [$verify_result.size_info]" print $"Timestamp: [$(get-timestamp)]" }