Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Commit

Permalink
pgbackups
Browse files Browse the repository at this point in the history
  • Loading branch information
will committed Mar 29, 2011
1 parent e61fd99 commit a620b91
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 147 deletions.
2 changes: 2 additions & 0 deletions init.rb
@@ -1,4 +1,6 @@
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'addons'
require 'pgresolver'
require 'pg'
require 'client'
require 'pgbackups'
150 changes: 3 additions & 147 deletions pg.rb
@@ -1,6 +1,7 @@
module Heroku
module Command
class Pg
include PGResolver
def ingress
uri = generate_ingress_uri("Granting ingress for 60s")
display "Connection info string:"
Expand All @@ -25,7 +26,7 @@ def wait

def promote
old_db = Resolver.new("DATABASE", config_vars)
new_db = resolve_db(:required => 'promote')
new_db = resolve_db(:required => 'pg:promote')
abort( " ! DATABASE_URL is already set to #{new_db[:name]}") if new_db[:default]

display "Promoting DATABASE_URL to #{new_db[:name]}"
Expand All @@ -38,7 +39,7 @@ def promote
end

def reset
db = resolve_db(:required => 'reset')
db = resolve_db(:required => 'pg:reset')

display "Resetting #{db[:pretty_name]}"
return unless confirm_command
Expand All @@ -54,32 +55,6 @@ def reset

private

def resolve_db(options={})
db_id = db_flag
unless db_id
if options[:allow_default]
db_id = "DATABASE"
else
abort(" ! Usage: heroku pg:#{options[:required]} --db <DATABASE>") if options[:required]
end
end
config_vars = heroku.config_vars(app)

resolver = Resolver.new(db_id, config_vars)
display resolver.message
unless resolver.url
display " ! Could not resolve database #{db_id}"
display " !"
display " ! Available databases: "
Resolver.all(config_vars).each do |db|
display " ! #{db[:pretty_name]}"
end
abort
end

return resolver
end

def promote_old_to_new(old_db, new_db)
return if [new_db, old_db].map(&:name).include? "SHARED_DATABASE"
working_display "Promoting" do
Expand All @@ -103,22 +78,6 @@ def heroku_postgresql_client(url)
HerokuPostgresql::Client10.new(url)
end

def specified_db?
db_flag
end

def db_flag
@db_flag ||= extract_option("--db")
end

def specified_db_or_all
if specified_db?
yield resolve_db
else
Resolver.all(config_vars).each { |db| yield db }
end
end

def wait_for(db)
return if "SHARED_DATABASE" == db[:name]
name = "database #{db[:pretty_name]}"
Expand Down Expand Up @@ -187,109 +146,6 @@ def generate_ingress_uri(action)
return URI.parse(db[:url])
end

def display(message, newline=true)
super if message
end

class Resolver
attr_reader :url, :db_id

def initialize(db_id, config_vars)
@db_id, @config_vars = db_id.upcase, config_vars
@messages = []
parse_config
resolve
end

def message
@messages.join("\n") unless @messages.empty?
end

def [](arg)
{ :name => name,
:url => url,
:pretty_name => pretty_name,
:default => default?
}[arg]
end

def name
db_id
end

def pretty_name
"#{db_id}#{ " (DATABASE_URL)" if default? }"
end

def self.all(config_vars)
parsed = parse_config(config_vars)
default = parsed['DATABASE']
dbs = []
parsed.reject{|k,v| k == 'DATABASE'}.each do |name, url|
dbs << {:name => name, :url => url, :default => url==default, :pretty_name => "#{name}#{' (DATABASE_URL)' if url==default}"}
end
dbs.sort {|a,b| a[:default]? -1 : a[:name] <=> b[:name] }
end

private

def parse_config
@dbs = self.class.parse_config(@config_vars)
end

def self.parse_config(config_vars)
dbs = {}
config_vars.each do |key,val|
case key
when "DATABASE_URL"
dbs['DATABASE'] = val
when 'SHARED_DATABASE_URL'
dbs['SHARED_DATABASE'] = val
when /^HEROKU_POSTGRESQL_(\w+)_URL$/
dbs[$+] = val # $+ is the last match
end
end
return dbs
end

def default?
url && url == @dbs['DATABASE']
end

def resolve
url_deprecation_check
default_database_check
h_pg_color_check
@url = @dbs[@db_id]
end

def url_deprecation_check
return unless @db_id =~ /(\w+)_URL$/
old_id = @db_id
@db_id = $+
@messages << "#{old_id} is deprecated, please use #{@db_id}"
end

def default_database_check
return unless @db_id == 'DATABASE'
dbs = @dbs.find { |k,v|
v == @dbs['DATABASE'] && k != 'DATABASE'
}

if dbs
@db_id = dbs.first
@messages << "using #{@db_id}"
else
@messages << "DATABASE_URL does not match any of your databases"
end
end

def h_pg_color_check
return unless @db_id =~ /^HEROKU_POSTGRESQL_(\w+)/
@db_id = $+
@messages << "using #{@db_id}"
end
end
end
end
end
Expand Down
88 changes: 88 additions & 0 deletions pgbackups.rb
@@ -0,0 +1,88 @@
require "heroku/commands/help"
require "heroku/pgutils"
require "pgbackups/client"

module Heroku::Command
class Pgbackups < BaseWithApp
include PGResolver

Heroku::Command::Help.group("pgbackups") do |group|
group.command "pgbackups:capture [--db <DB_ID>]", "capture a backup from database ID (default: DATABASE_URL)"
end

def 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 += "\n ! The database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/
message += "\n ! The database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/
abort(message)
end

end

def restore
db = resolve_db(:allow_default => true)
to_name = db[:name]
to_url = db[:url]

backup_id = args.shift

if backup_id =~ /^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
if backup_id
backup = pgbackup_client.get_backup(backup_id)
abort("Backup #{backup_id} already deleted.") if backup["destroyed_at"]
else
backup = pgbackup_client.get_latest_backup
to_uri = URI.parse backup["to_url"]
backup_id = File.basename(to_uri.path, '.*')
backup_id = "#{backup_id} (most recent)"
end

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 += "\n ! The backup url is invalid. Use `pgbackups:url` to generate a new temporary URL." if restore['log'] =~ /Invalid dump format: .*: XML document text/
abort(message)
end
end
end

end
end

0 comments on commit a620b91

Please sign in to comment.