manage backups of heroku postgresql databases
pgbackups:capture [DATABASE]
capture a backup from a database id
if no DATABASE is specified, defaults to DATABASE_URL
-e, --expire # if no slots are available to capture, destroy the oldest backup to make room
# File lib/heroku/command/pgbackups.rb, line 57 def capture deprecate_dash_dash_db("pgbackups:capture") db = resolve_db(:allow_default => true) from_url = db[:url] from_name = db[:name] to_url = nil # server will assign to_name = "BACKUP" opts = {:expire => extract_option("--expire")} backup = transfer!(from_url, from_name, to_url, to_name, opts) to_uri = URI.parse backup["to_url"] backup_id = to_uri.path.empty? ? "error" : File.basename(to_uri.path, '.*') display "\n#{db[:pretty_name]} ----backup---> #{backup_id}" backup = poll_transfer!(backup) if backup["error_at"] message = "An error occurred and your backup did not finish." message += "\nThe database is not yet online. Please try again." if backup['log'] =~ %rName or service not known/ message += "\nThe database credentials are incorrect." if backup['log'] =~ %rpsql: FATAL:/ error(message) end end
pgbackups:destroy BACKUP_ID
destroys a backup
# File lib/heroku/command/pgbackups.rb, line 154 def destroy name = args.shift abort("Backup name required") unless name backup = pgbackup_client.get_backup(name) abort("Backup #{name} already destroyed.") if backup["destroyed_at"] result = pgbackup_client.delete_backup(name) if result display("Backup #{name} destroyed.") else abort("Error deleting backup #{name}.") end end
pgbackups
list captured backups
# File lib/heroku/command/pgbackups.rb, line 17 def index backups = [] pgbackup_client.get_transfers.each { |t| next unless backup_types.member?(t['to_name']) && !t['error_at'] && !t['destroyed_at'] backups << [backup_name(t['to_url']), t['created_at'], t['size'], t['from_name'], ] } if backups.empty? no_backups_error! else display Display.new.render([["ID", "Backup Time", "Size", "Database"]], backups) end end
pgbackups:restore [<DATABASE> [BACKUP_ID|BACKUP_URL]]
restore a backup to a database
if no DATABASE is specified, defaults to DATABASE_URL and latest backup if DATABASE is specified, but no BACKUP_ID, defaults to latest backup
# File lib/heroku/command/pgbackups.rb, line 91 def restore deprecate_dash_dash_db("pgbackups:restore") if 0 == args.size db = resolve_db(:allow_default => true) backup_id = :latest elsif 1 == args.size db = resolve_db backup_id = :latest else db = resolve_db backup_id = args.shift end to_name = db[:name] to_url = db[:url] if :latest == backup_id backup = pgbackup_client.get_latest_backup no_backups_error! if {} == backup to_uri = URI.parse backup["to_url"] backup_id = File.basename(to_uri.path, '.*') backup_id = "#{backup_id} (most recent)" from_url = backup["to_url"] from_name = "BACKUP" elsif backup_id =~ %r^http(s?):\/\// from_url = backup_id from_name = "EXTERNAL_BACKUP" from_uri = URI.parse backup_id backup_id = from_uri.path.empty? ? from_uri : File.basename(from_uri.path) else backup = pgbackup_client.get_backup(backup_id) abort("Backup #{backup_id} already destroyed.") if backup["destroyed_at"] from_url = backup["to_url"] from_name = "BACKUP" end message = "#{db[:pretty_name]} <---restore--- " padding = " " * message.length display "\n#{message}#{backup_id}" if backup display padding + "#{backup['from_name']}" display padding + "#{backup['created_at']}" display padding + "#{backup['size']}" end if confirm_command restore = transfer!(from_url, from_name, to_url, to_name) restore = poll_transfer!(restore) if restore["error_at"] message = "An error occurred and your restore did not finish." message += "\nThe backup url is invalid. Use `pgbackups:url` to generate a new temporary URL." if restore['log'] =~ %rInvalid dump format: .*: XML document text/ error(message) end end end
pgbackups:url [BACKUP_ID]
get a temporary URL for a backup
# File lib/heroku/command/pgbackups.rb, line 35 def url if name = args.shift b = pgbackup_client.get_backup(name) else b = pgbackup_client.get_latest_backup end abort("No backup found.") unless b['public_url'] if STDOUT.isatty display '"'+b['public_url']+'"' else display b['public_url'] end end
# File lib/heroku/command/pgbackups.rb, line 180 def backup_name(to_url) # translate s3://bucket/email/foo/bar.dump => foo/bar parts = to_url.split('/') parts.slice(4..-1).join('/').gsub(%r\.dump$/, '') end
# File lib/heroku/command/pgbackups.rb, line 170 def config_vars @config_vars ||= heroku.config_vars(app) end
# File lib/heroku/command/pgbackups.rb, line 174 def pgbackup_client pgbackups_url = ENV["PGBACKUPS_URL"] || config_vars["PGBACKUPS_URL"] error("Please add the pgbackups addon first via:\nheroku addons:add pgbackups") unless pgbackups_url @pgbackup_client ||= PGBackups::Client.new(pgbackups_url) end
# File lib/heroku/command/pgbackups.rb, line 190 def poll_transfer!(transfer) display "\n" if transfer["errors"] transfer["errors"].values.flatten.each { |e| output_with_bang "#{e}" } abort end while true update_display(transfer) break if transfer["finished_at"] sleep 1 transfer = pgbackup_client.get_transfer(transfer["id"]) end display "\n" return transfer end
# File lib/heroku/command/pgbackups.rb, line 186 def transfer!(from_url, from_name, to_url, to_name, opts={}) pgbackup_client.create_transfer(from_url, from_name, to_url, to_name, opts) end
# File lib/heroku/command/pgbackups.rb, line 213 def update_display(transfer) @ticks ||= 0 @last_updated_at ||= 0 @last_logs ||= [] @last_progress ||= ["", 0] @ticks += 1 step_map = { "dump" => "Capturing", "upload" => "Storing", "download" => "Retrieving", "restore" => "Restoring", "gunzip" => "Uncompressing", "load" => "Restoring", } if !transfer["log"] @last_progress = ['pending', nil] redisplay "Pending... #{spinner(@ticks)}" else logs = transfer["log"].split("\n") new_logs = logs - @last_logs @last_logs = logs new_logs.each do |line| matches = line.scan %r^([a-z_]+)_progress:\s+([^ ]+)/ next if matches.empty? step, amount = matches[0] if ['done', 'error'].include? amount # step is done, explicitly print result and newline redisplay "#{@last_progress[0].capitalize}... #{amount}\n" end # store progress, last one in the logs will get displayed step = step_map[step] || step @last_progress = [step, amount] end step, amount = @last_progress unless ['done', 'error'].include? amount redisplay "#{step.capitalize}... #{amount} #{spinner(@ticks)}" end end end