Last active
July 6, 2024 11:55
-
-
Save zavan/ab00ddb0d1c628430b43c46a25eba495 to your computer and use it in GitHub Desktop.
Poor man's Rails backup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require "bundler/inline" | |
require "logger" | |
gemfile do | |
source "https://rubygems.org" | |
gem "bcrypt_pbkdf" | |
gem "ed25519" | |
gem "net-scp" | |
gem "net-ssh" | |
end | |
host = ARGV[0] | |
user = ARGV[1] | |
key = ARGV[2] | |
db_uri = ARGV[3] | |
app_dir = ARGV[4] | |
output_dir = ARGV[5] | |
backup_pass = ARGV[6] | |
retention = ARGV[7].to_i | |
class Backup | |
attr_reader :host, :user, :key, :db_uri, :app_dir, :output_dir, :backup_pass, :retention, :logger | |
def initialize(host:, user:, key:, db_uri:, app_dir:, output_dir:, backup_pass:, retention: 7) | |
@host = host | |
@user = user | |
@key = key | |
@db_uri = db_uri | |
@app_dir = app_dir | |
@output_dir = output_dir | |
@retention = retention | |
@backup_pass = backup_pass | |
@logger = Logger.new(STDOUT) | |
@logger.level = Logger::INFO | |
@work_dir = "/tmp" | |
@tarball_name = "rails_backup-#{Time.now.strftime("%Y%m%d%H%M%S")}.tar.gz" | |
@tarball_path = File.join(@work_dir, @tarball_name) | |
@db_dump_name = "rails_db_dump.sql" | |
@db_dump_path = File.join(@work_dir, @db_dump_name) | |
end | |
def self.decrypt(file, pass) | |
enc_tarball_path = file | |
tarball_path = enc_tarball_path.sub(".gpg.tar.gz", ".tar.gz") | |
command = "gpg --batch --yes --output #{tarball_path} --decrypt --passphrase #{pass} #{enc_tarball_path}" | |
system(command) | |
tarball_path | |
end | |
def run | |
logger.info "Starting backup..." | |
pg_dump | |
tar | |
encrypt | |
download | |
cleanup_server | |
cleanup_retention | |
logger.info "Backup finished successfully" | |
ensure | |
ssh.close | |
end | |
def pg_dump | |
channel_exec("pg_dump", "pg_dump #{db_uri} > #{@db_dump_path}") | |
end | |
def tar | |
channel_exec("tar", "tar -czf #{@tarball_path} -C #{app_dir} storage -C /tmp #{@db_dump_name}") | |
end | |
def encrypt | |
logger.info "Encrypting tarball..." | |
@enc_tarball_name = @tarball_name.sub(".tar.gz", ".gpg.tar.gz") | |
@enc_tarball_path = File.join(@work_dir, @enc_tarball_name) | |
command = "gpg --batch --yes --output #{@enc_tarball_path} --symmetric --cipher-algo AES256 --passphrase #{@backup_pass} #{@tarball_path}" | |
channel_exec("encrypt", command) | |
end | |
def download | |
output_path = File.join(output_dir, @enc_tarball_name) | |
logger.info "Downloading tarball to #{output_path}..." | |
ssh.scp.download!(@enc_tarball_path, output_path) do |ch, name, dowloaded, total| | |
# print "\rDownloading #{name}: #{dowloaded}/#{total}" | |
end | |
puts | |
end | |
def cleanup_server | |
logger.info "Cleaning up server..." | |
ssh.exec!("rm #{@db_dump_path} #{@tarball_path} #{@enc_tarball_path}") | |
end | |
def cleanup_retention | |
logger.info "Retaining #{retention} backups..." | |
backups = Dir.glob(File.join(output_dir, "rails_backup-*.tar.gz")).sort | |
to_delete = backups[0...-retention] | |
logger.info "Deleting #{to_delete.size} old backups..." | |
to_delete.each do |old| | |
logger.info "Deleting #{old}..." | |
File.delete(old) | |
end | |
end | |
private | |
def ssh | |
@ssh ||= Net::SSH.start( | |
host, | |
user, | |
keys: [key], | |
keys_only: true, | |
verbose: :warn | |
) | |
end | |
def channel_exec(name, command) | |
channel = ssh.open_channel do |ch| | |
logger.info "Running #{name}..." | |
ch.exec(command) do |ch, success| | |
raise "Could not start #{name}" unless success | |
ch.on_data { |c, data| logger.info data } | |
ch.on_extended_data { |c, type, data| logger.warn data } | |
ch.on_request("exit-status") do |c, data| | |
status = data.read_long | |
if status.zero? | |
logger.info "#{name} finished successfully" | |
else | |
raise "#{name} failed with status #{status}" | |
end | |
end | |
end | |
end | |
channel.wait | |
end | |
end | |
Backup.new( | |
host:, | |
user:, | |
key:, | |
db_uri:, | |
app_dir:, | |
output_dir:, | |
retention:, | |
backup_pass: | |
).run | |
# Backup.decrypt("/path/to/rails_backup-20240706124029.gpg.tar.gz", "password") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You don't pay for RDS. You don't pay for S3. You don't pay for AWS Backup/snapshots...
Run this with a cron on your machine (we both know you never turn it off) to download an encrypted backup of your PostgreSQL database and ActiveStorage file system uploads.