Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
mdBook Build & Deploy / Build mdBook (push) Has been cancelled
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
mdBook Build & Deploy / Documentation Quality Check (push) Has been cancelled
mdBook Build & Deploy / Deploy to GitHub Pages (push) Has been cancelled
mdBook Build & Deploy / Notification (push) Has been cancelled
285 lines
6.8 KiB
Plaintext
285 lines
6.8 KiB
Plaintext
#!/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)]"
|
|
}
|