glacier_on_rails 0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +231 -0
- data/MIT-LICENSE +20 -0
- data/README.md +98 -0
- data/Rakefile +13 -0
- data/app/assets/images/glacier_on_rails/.gitkeep +0 -0
- data/app/assets/javascripts/glacier_on_rails/application.js +15 -0
- data/app/assets/stylesheets/glacier_on_rails/application.css +13 -0
- data/app/controllers/glacier_on_rails/application_controller.rb +2 -0
- data/app/controllers/glacier_on_rails/application_data_backups_controller.rb +43 -0
- data/app/controllers/glacier_on_rails/aws_archive_retrieval_jobs_controller.rb +7 -0
- data/app/controllers/glacier_on_rails/aws_sns_subscriptions_controller.rb +40 -0
- data/app/helpers/glacier_on_rails/application_helper.rb +4 -0
- data/app/models/application_data_backup.rb +84 -0
- data/app/models/application_database/base_adapter.rb +7 -0
- data/app/models/application_database/mysql_adapter.rb +11 -0
- data/app/models/application_database/postgres_adapter.rb +52 -0
- data/app/models/application_database.rb +29 -0
- data/app/models/application_file.rb +25 -0
- data/app/models/aws_backend/sns_subscription.rb +54 -0
- data/app/models/aws_backend.rb +113 -0
- data/app/models/aws_log.rb +7 -0
- data/app/models/glacier_archive.rb +115 -0
- data/app/models/glacier_db_archive.rb +27 -0
- data/app/models/glacier_file_archive.rb +45 -0
- data/app/views/glacier_on_rails/aws_archive_retrieval_jobs/_application_data_backup.haml +4 -0
- data/app/views/glacier_on_rails/aws_archive_retrieval_jobs/_available.haml +3 -0
- data/app/views/glacier_on_rails/aws_archive_retrieval_jobs/_index.haml +101 -0
- data/app/views/glacier_on_rails/aws_archive_retrieval_jobs/_local.haml +1 -0
- data/app/views/glacier_on_rails/aws_archive_retrieval_jobs/_pending.haml +1 -0
- data/app/views/glacier_on_rails/aws_archive_retrieval_jobs/_ready.haml +2 -0
- data/config/initializers/aws_log.rb +1 -0
- data/config/initializers/glacier_on_rails.rb +3 -0
- data/config/initializers/time_formats.rb +3 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20170503133854_create_glacier_archives_table.rb +11 -0
- data/db/migrate/20170507161533_add_notification_column_to_glacier_archives.rb +5 -0
- data/db/migrate/20170509195716_add_archive_retrieval_job_id_to_glacier_archive.rb +5 -0
- data/db/migrate/20170602135721_create_application_data_backups.rb +7 -0
- data/db/migrate/20170602143524_add_application_data_backup_id_to_glacier_archives.rb +5 -0
- data/db/migrate/20170602145526_create_glacier_file_archive_join_table.rb +5 -0
- data/db/migrate/20170602150324_add_type_to_glacier_archives.rb +5 -0
- data/db/migrate/20170603141909_add_filename_to_glacier_archive.rb +5 -0
- data/glacier_on_rails.gemspec +29 -0
- data/lib/glacier_on_rails/config.rb +22 -0
- data/lib/glacier_on_rails/engine.rb +5 -0
- data/lib/glacier_on_rails/version.rb +3 -0
- data/lib/glacier_on_rails.rb +5 -0
- data/lib/tasks/aws.rake +6 -0
- data/script/rails +8 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/javascripts/ractive.js +16621 -0
- data/spec/dummy/app/assets/javascripts/underscore.js +5 -0
- data/spec/dummy/app/assets/stylesheets/application.css +14 -0
- data/spec/dummy/app/controllers/admin_controller.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +6 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/fake_model.rb +10 -0
- data/spec/dummy/app/views/admin/_flash_error.haml +40 -0
- data/spec/dummy/app/views/admin/index.haml +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +67 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +6 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +40 -0
- data/spec/dummy/config/environments/production.rb +70 -0
- data/spec/dummy/config/environments/test.rb +40 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/file_upload.rb +1 -0
- data/spec/dummy/config/initializers/glacier_on_rails.rb +5 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +8 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20170531170208_create_fake_models.rb +8 -0
- data/spec/dummy/db/schema.rb +56 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/lib/constants.rb +2 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/features/application_data_backup_spec.rb +166 -0
- data/spec/helpers/aws_helper.rb +99 -0
- data/spec/helpers/dummy_app_helper.rb +23 -0
- data/spec/helpers/http_mock_helpers.rb +137 -0
- data/spec/models/application_data_backup_spec.rb +226 -0
- data/spec/models/application_file_spec.rb +8 -0
- data/spec/models/glacier_db_archive_spec.rb +118 -0
- data/spec/models/glacier_file_archive_spec.rb +56 -0
- data/spec/models/postgres_adapter_spec.rb +46 -0
- data/spec/rails_helper.rb +59 -0
- data/spec/spec_helper.rb +110 -0
- data/spec/support/wait_for_ajax.rb +22 -0
- metadata +308 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
class ApplicationDatabase::PostgresAdapter < ApplicationDatabase::BaseAdapter
|
2
|
+
class PgDumpCmdMissing < StandardError; end
|
3
|
+
class PgRestoreCmdMissing < StandardError; end
|
4
|
+
class PgRestoreFileMissing < StandardError; end
|
5
|
+
|
6
|
+
RestoreExclusions = %w{ application_data_backups
|
7
|
+
glacier_archives
|
8
|
+
SCHEMA }
|
9
|
+
|
10
|
+
RestoreList = GlacierArchive::BackupFileDir.join('restore.list')
|
11
|
+
|
12
|
+
def contents
|
13
|
+
`#{pg_dump} -w -Fc -U #{db_config['username']} #{db_config['database']}`
|
14
|
+
end
|
15
|
+
|
16
|
+
def restore(file)
|
17
|
+
raise PgRestoreFileMissing unless File.exists? file
|
18
|
+
restore_from_list(file)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def generate_object_restoral_list(file)
|
23
|
+
# generate the raw list of all objects
|
24
|
+
system("#{pg_restore} --clean -n public -l #{file} > #{RestoreList}")
|
25
|
+
# remove objects from the list that are enumerated in RestoreExclusions
|
26
|
+
retained_lines = File.readlines(RestoreList).select{|line| !line.match(/#{RestoreExclusions.join("|")}/)}
|
27
|
+
File.open RestoreList, 'w' do |file|
|
28
|
+
file.write(retained_lines.join)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def restore_from_list(file)
|
33
|
+
generate_object_restoral_list(file)
|
34
|
+
result = `#{pg_restore} --clean -n public -U #{db_config['username']} --dbname=#{db_config['database']} -L #{RestoreList} #{file} 2>&1`
|
35
|
+
`rm #{RestoreList} && rm #{file}` if $?.exitstatus.zero?
|
36
|
+
AwsLog.error result unless $?.exitstatus.zero?
|
37
|
+
$?.exitstatus.zero?
|
38
|
+
end
|
39
|
+
|
40
|
+
def pg_dump
|
41
|
+
dump_cmd = `which pg_dump`.strip
|
42
|
+
raise PgDumpCmdMissing if dump_cmd.blank?
|
43
|
+
dump_cmd
|
44
|
+
end
|
45
|
+
|
46
|
+
def pg_restore
|
47
|
+
restore_cmd = `which pg_restore`.strip
|
48
|
+
raise PgRestoreCmdMissing if restore_cmd.blank?
|
49
|
+
restore_cmd
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class ApplicationDatabase
|
2
|
+
class MissingConfigurationKeys < StandardError
|
3
|
+
def initialize(missing_keys)
|
4
|
+
msg = "#{missing_keys.join(' and ')} must be specified in config/database.yml"
|
5
|
+
super(msg)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :adapter
|
10
|
+
RequiredKeys = %w{username database adapter}
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@adapter = case db_config["adapter"]
|
14
|
+
when "postgresql"
|
15
|
+
PostgresAdapter.new(db_config)
|
16
|
+
when "mysql"
|
17
|
+
MysqlAdapter.new(db_config)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def db_config
|
22
|
+
config = ActiveRecord::Base.configurations[Rails.env]
|
23
|
+
missing_keys = RequiredKeys - config.keys
|
24
|
+
raise MissingConfigurationKeys.new(missing_keys) unless missing_keys.empty?
|
25
|
+
config
|
26
|
+
end
|
27
|
+
|
28
|
+
delegate :contents, :restore, :to => :adapter
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'glacier_on_rails/config'
|
2
|
+
class ApplicationFile
|
3
|
+
attr_accessor :file
|
4
|
+
def initialize(file)
|
5
|
+
# file may be a Pathname instance or a string
|
6
|
+
@file = file.to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
def contents
|
10
|
+
ActiveSupport::Gzip.compress(File.read(file))
|
11
|
+
end
|
12
|
+
|
13
|
+
def filename
|
14
|
+
File.basename(file)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.list
|
18
|
+
files.map{|f| new(f) }.map(&:filename)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.files
|
22
|
+
Dir.glob(GlacierOnRails::Config.attached_files_directory.join('*'))
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class AwsBackend
|
2
|
+
class SnsSubscription
|
3
|
+
attr_accessor :resp
|
4
|
+
# format is: "arn:aws:sns:region:account-id:topicname"
|
5
|
+
# see http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arn-syntax-glacier
|
6
|
+
Topic_ARN = "arn:aws:sns:#{AwsBackend::Config.aws_region}:#{::AWS_ACCOUNT_ID}:retrieve_archive"
|
7
|
+
#Resource_ARN = ""
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
client = Aws::SNS::Client.new(:region => AwsBackend::Config.aws_region, :credentials => credentials)
|
11
|
+
sns = Aws::SNS::Resource.new(:region => AwsBackend::Config.aws_region)
|
12
|
+
|
13
|
+
topic = sns.create_topic(name: 'retrieve_archive')
|
14
|
+
topic.set_attributes({
|
15
|
+
attribute_name: "Policy",
|
16
|
+
attribute_value: policy
|
17
|
+
})
|
18
|
+
|
19
|
+
@resp = client.subscribe({
|
20
|
+
topic_arn: Topic_ARN,
|
21
|
+
protocol: "https", # required
|
22
|
+
endpoint: GlacierOnRails::Engine.routes.url_helpers.aws_subscription_notify_url
|
23
|
+
})
|
24
|
+
rescue Exception => e
|
25
|
+
puts "AWS Error: #{e.message}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def credentials
|
30
|
+
Aws::SharedCredentials.new(:profile_name => AwsBackend::Config.profile_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def policy
|
34
|
+
# see http://docs.aws.amazon.com/sdk-for-ruby/v2/developer-guide/sns-example-enable-resource.html
|
35
|
+
"{
|
36
|
+
'Version':'2014-05-06',
|
37
|
+
'Id':'__default_policy_ID',
|
38
|
+
'Statement':[{
|
39
|
+
'Sid':'__default_statement_ID',
|
40
|
+
'Effect':'Allow',
|
41
|
+
'Principal':{
|
42
|
+
'AWS':'*'
|
43
|
+
},
|
44
|
+
'Action':['SNS:Publish'],
|
45
|
+
'Resource':'#{Topic_ARN}',
|
46
|
+
'Condition':{
|
47
|
+
'ArnEquals':{
|
48
|
+
'AWS:SourceArn': #{Resource_ARN}}
|
49
|
+
}
|
50
|
+
}]
|
51
|
+
}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'glacier_on_rails/config'
|
2
|
+
|
3
|
+
class AwsBackend
|
4
|
+
class ArchiveRetrievalNotReady < StandardError; end
|
5
|
+
class Config < GlacierOnRails::Config; end
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
attr_accessor :client, :error_message
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@client = Aws::Glacier::Client.new(:region => Config.aws_region, :credentials => AwsBackend.credentials)
|
12
|
+
unless vault_list.map(&:vault_name).include?(::SITE_NAME)
|
13
|
+
@client.create_vault({:account_id => "-", :vault_name => ::SITE_NAME})
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.credentials
|
18
|
+
Aws::SharedCredentials.new(:profile_name => Config.profile_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def vault_list
|
22
|
+
vault_list_info = @client.list_vaults({:account_id => "-"})
|
23
|
+
vault_list_info.vault_list
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete_archive(archive)
|
27
|
+
begin
|
28
|
+
response = client.delete_archive({
|
29
|
+
account_id: "-",
|
30
|
+
archive_id: archive.archive_id,
|
31
|
+
vault_name: ::SITE_NAME
|
32
|
+
})
|
33
|
+
AwsLog.info "Delete archive response: #{response}"
|
34
|
+
response
|
35
|
+
rescue Aws::Glacier::Errors::ServiceError => e
|
36
|
+
AwsLog.error "Failed to delete archive with: #{e.class}: #{e.message}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_archive(archive_contents)
|
41
|
+
description = "backup of postgres database"
|
42
|
+
begin
|
43
|
+
resp = client.upload_archive({
|
44
|
+
account_id: "-",
|
45
|
+
archive_description: description,
|
46
|
+
body: archive_contents,
|
47
|
+
checksum: checksum(archive_contents),
|
48
|
+
vault_name: ::SITE_NAME
|
49
|
+
})
|
50
|
+
AwsLog.info "Create archive response: #{resp}"
|
51
|
+
resp
|
52
|
+
rescue Aws::Glacier::Errors::ServiceError => e
|
53
|
+
self.error_message = "Failed to create archive with: #{e.class}: #{e.message}"
|
54
|
+
AwsLog.error error_message
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# archive is a GlacierArchive instance from the database
|
60
|
+
# response looks like this:
|
61
|
+
# <struct Aws::Glacier::Types::InitiateJobOutput location="/918359762546/vaults/demo/jobs/krCLWk6m7NJWppiy2SxdhP60f98PdrdaZBfhdDTZufrAkoh-ikrvb_NA0Q1vg2WcAhzZLL92kiwjOijUEDh0U7X09YQK", job_id="krCLWk6m7NJWppiy2SxdhP60f98PdrdaZBfhdDTZufrAkoh-ikrvb_NA0Q1vg2WcAhzZLL92kiwjOijUEDh0U7X09YQK">
|
62
|
+
def retrieve_archive(archive)
|
63
|
+
response = client.initiate_job({ account_id: "-", # required
|
64
|
+
vault_name: ::SITE_NAME, # required
|
65
|
+
job_parameters: {
|
66
|
+
type: "archive-retrieval", # valid types are "archive-retrieval" and "inventory-retrieval"
|
67
|
+
archive_id: archive.archive_id,
|
68
|
+
description: "put anything here",
|
69
|
+
sns_topic: SnsSubscription::Topic_ARN,
|
70
|
+
tier: "Standard"# it's the default, but put it here to be explicit
|
71
|
+
}
|
72
|
+
})
|
73
|
+
AwsLog.info "Retrieve archive response: #{response}"
|
74
|
+
response
|
75
|
+
rescue Aws::Glacier::Errors::ServiceError => e
|
76
|
+
self.error_message = "Failed to initiate archive retrieval with: #{e.class}: #{e.message}"
|
77
|
+
AwsLog.error error_message
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
# archive is a GlacierArchive instance from the database
|
82
|
+
def get_job_output(archive)
|
83
|
+
raise ArchiveRetrievalNotReady unless archive.retrieval_status == 'ready'
|
84
|
+
params = {
|
85
|
+
:response_target => filepath(archive),
|
86
|
+
:account_id => '-',
|
87
|
+
:vault_name => ::SITE_NAME,
|
88
|
+
:job_id => archive.archive_retrieval_job_id
|
89
|
+
}
|
90
|
+
AwsLog.info("AWS Backend get job output request with: #{params}")
|
91
|
+
resp = client.get_job_output(params)
|
92
|
+
AwsLog.info("AWS Backend response to get job output request: #{resp}")
|
93
|
+
resp
|
94
|
+
rescue ArchiveRetrievalNotReady
|
95
|
+
AwsLog.error "Get job output failed, archive status is not 'ready'"
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def checksum(contents)
|
102
|
+
tree_hash = Aws::TreeHash.new
|
103
|
+
tree_hash.update(contents)
|
104
|
+
tree_hash.digest
|
105
|
+
end
|
106
|
+
|
107
|
+
def filepath(archive)
|
108
|
+
path = archive.backup_file
|
109
|
+
FileUtils.makedirs(File.dirname(path))
|
110
|
+
path
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# GlacierArchive lifecycle:
|
2
|
+
# ACTION | IMPLEMENTED BY | PARAMETER | STATUS INDICATION | STATUS
|
3
|
+
# create | AwsBackend#create_archive | | default attributes | available
|
4
|
+
# initiate retrieval job | AwsBackend#retrieve_archive | archive_id | archive_retrieval_job_id | pending
|
5
|
+
# SNS notification received | AwsSnsSubscriptionsController | archive_retrieval_job_id | notification attribute | ready
|
6
|
+
# fetch archive | AwsArchiveController#fetch | archive_retrieval_job_id | retrieved file exists locally | local
|
7
|
+
# | | | original file exists locally | exists
|
8
|
+
class GlacierArchive < ActiveRecord::Base
|
9
|
+
class RestoreFail < StandardError; end
|
10
|
+
|
11
|
+
default_scope ->{ order("created_at asc") }
|
12
|
+
|
13
|
+
BackupFileDir = Rails.root.join('tmp','aws')
|
14
|
+
|
15
|
+
before_create do |archive|
|
16
|
+
# create the archive at AWS Glacier
|
17
|
+
# and save the metadata in the GlacierArchive instance
|
18
|
+
# archive_contents provided by subclass (GlacierDbArchive or GlacierFileArchive)
|
19
|
+
if archive_contents && (resp = aws.create_archive(archive_contents))
|
20
|
+
archive.attributes = resp.to_h
|
21
|
+
else
|
22
|
+
archive.errors.add(:base, aws.error_message)
|
23
|
+
throw :abort
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
after_destroy do |archive|
|
28
|
+
archive.remove_local_backup_copy
|
29
|
+
end
|
30
|
+
|
31
|
+
before_destroy do |archive|
|
32
|
+
archive.destroy_archive
|
33
|
+
end
|
34
|
+
|
35
|
+
def destroy_archive
|
36
|
+
begin
|
37
|
+
response = aws.delete_archive(self)
|
38
|
+
AwsLog.info(response)
|
39
|
+
true
|
40
|
+
rescue Aws::Glacier::Errors::ServiceError => e
|
41
|
+
AwsLog.error("Delete archived failed with :#{e.class}: #{e.message}")
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initiate_retrieve_job
|
47
|
+
if resp = aws.retrieve_archive(self)
|
48
|
+
update_attribute(:archive_retrieval_job_id, resp[:job_id])
|
49
|
+
else
|
50
|
+
errors.add(:base, aws.error_message)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def fetch_archive
|
55
|
+
begin
|
56
|
+
response = aws.get_job_output(self)
|
57
|
+
AwsLog.info(response.to_h) if response
|
58
|
+
true
|
59
|
+
rescue Aws::Glacier::Errors::ServiceError => e
|
60
|
+
self.errors.add(:base, e.message)
|
61
|
+
AwsLog.error("Fetch archive failed with: #{e.class}: #{e.message}")
|
62
|
+
false
|
63
|
+
ensure
|
64
|
+
reset_retrieval_status
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def retrieval_status
|
69
|
+
exists_status || local_status || ready_status || pending_status || 'available'
|
70
|
+
end
|
71
|
+
|
72
|
+
def backup_file
|
73
|
+
BackupFileDir.join(filename)
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
|
78
|
+
def remove_local_backup_copy
|
79
|
+
FileUtils.rm(backup_file) if local_status?
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def aws
|
85
|
+
AwsBackend.instance
|
86
|
+
end
|
87
|
+
|
88
|
+
def reset_retrieval_status
|
89
|
+
# because error messages will be reset when update_attributes is used
|
90
|
+
update_columns(:archive_retrieval_job_id => nil, :notification => nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
def exists_status
|
94
|
+
'exists' if exists_status?
|
95
|
+
end
|
96
|
+
|
97
|
+
def local_status?
|
98
|
+
File.exists? backup_file
|
99
|
+
end
|
100
|
+
|
101
|
+
# archive_retrieval job output has been retrieved
|
102
|
+
def local_status
|
103
|
+
'local' if local_status?
|
104
|
+
end
|
105
|
+
|
106
|
+
# ready to retrieve archive_retrieve job output
|
107
|
+
def ready_status
|
108
|
+
'ready' if notification
|
109
|
+
end
|
110
|
+
|
111
|
+
# archive_retrieve job has been initiated, but notification not yet received
|
112
|
+
def pending_status
|
113
|
+
'pending' if archive_retrieval_job_id
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class GlacierDbArchive < GlacierArchive
|
2
|
+
belongs_to :application_data_backup
|
3
|
+
|
4
|
+
before_create do |db_archive|
|
5
|
+
db_archive.filename = GlacierDbArchive.filename_from_time
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.filename_from_time
|
9
|
+
Time.now.strftime("%Y_%m_%d_%H_%M_%S_%L.sql").to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def archive_contents
|
13
|
+
ApplicationDatabase.new.contents
|
14
|
+
rescue ApplicationDatabase::MissingConfigurationKeys
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# for file archive, true inhibits fetching archive from AWS
|
19
|
+
# however we always want to fetch the db archive
|
20
|
+
def exists_status?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def restore
|
25
|
+
raise RestoreFail unless ApplicationDatabase.new.restore(backup_file)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'glacier_on_rails/config'
|
2
|
+
|
3
|
+
class GlacierFileArchive < GlacierArchive
|
4
|
+
has_and_belongs_to_many :application_data_backups, join_table: 'application_data_backups_glacier_file_archives'
|
5
|
+
|
6
|
+
def file=(file)
|
7
|
+
self.filename = File.basename(file)
|
8
|
+
end
|
9
|
+
|
10
|
+
def file
|
11
|
+
GlacierOnRails::Config.attached_files_directory.join(filename)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initiate_retrieve_job
|
15
|
+
# don't retrieve the file if it's in the filesystem already
|
16
|
+
# based on the assumption that files are immutable
|
17
|
+
super unless File.exists?(file)
|
18
|
+
end
|
19
|
+
|
20
|
+
# the bang method creates the instances if they didn't already exist
|
21
|
+
def self.all!
|
22
|
+
in_filesystem.collect do |file|
|
23
|
+
find_or_create_by(:filename => file)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def archive_contents
|
28
|
+
ApplicationFile.new(file).contents
|
29
|
+
end
|
30
|
+
|
31
|
+
def exists_status?
|
32
|
+
File.exists? file
|
33
|
+
end
|
34
|
+
|
35
|
+
def restore
|
36
|
+
FileUtils.mv(backup_file, file)
|
37
|
+
rescue Errno::ENOENT # usually b/c backup_file not found
|
38
|
+
raise RestoreFail
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def self.in_filesystem
|
43
|
+
ApplicationFile.list
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,4 @@
|
|
1
|
+
%tr.application_data_backup{:outro => :fade}
|
2
|
+
%td.date{:style => 'width:120px;'}= application_data_backup.created_at.to_date.to_s(:default)
|
3
|
+
= render :partial => "glacier_on_rails/aws_archive_retrieval_jobs/#{application_data_backup.retrieval_status}", :locals => {:application_data_backup => application_data_backup, :fetch_fail => local_assigns[:fetch_fail] || false}
|
4
|
+
%td.action_link#delete_archive{:onclick => "delete_archive(#{application_data_backup.id})", :style => 'padding-left: 20px;'} delete
|
@@ -0,0 +1,3 @@
|
|
1
|
+
%td#initiate_retrieval.action_link{:onclick=>"initiate_retrieval(#{application_data_backup.id})"} initiate retrieval
|
2
|
+
- if local_assigns[:fetch_fail]
|
3
|
+
%td#fetch_fail_message{:style => 'padding-left:25px;'} Fetch archive failed. The job probably expired. Re-initiate archive retrieval.
|
@@ -0,0 +1,101 @@
|
|
1
|
+
:css
|
2
|
+
.disabled {
|
3
|
+
pointer-events : none;
|
4
|
+
}
|
5
|
+
#scrim {
|
6
|
+
background-color : black;
|
7
|
+
opacity : 0.2;
|
8
|
+
position : absolute;
|
9
|
+
top : 0;
|
10
|
+
width : 100%;
|
11
|
+
height : 100%;
|
12
|
+
}
|
13
|
+
#spinner {
|
14
|
+
position:absolute;
|
15
|
+
color: white;
|
16
|
+
top:50%;
|
17
|
+
left:50%;
|
18
|
+
transform:translate(-50%,-50%)
|
19
|
+
}
|
20
|
+
%h2 AWS Glacier Archives
|
21
|
+
%table#application_data_backups
|
22
|
+
- if application_data_backups.empty?
|
23
|
+
%p None have been created
|
24
|
+
- else
|
25
|
+
= render :partial => 'glacier_on_rails/aws_archive_retrieval_jobs/application_data_backup', :collection => application_data_backups, :as => :application_data_backup
|
26
|
+
|
27
|
+
%div{:style => 'display:flex; margin-top:12px;'}
|
28
|
+
%div.action_link#backup_now{:onclick => 'backup_now()'} backup now
|
29
|
+
|
30
|
+
:coffeescript
|
31
|
+
@backup_now = ->
|
32
|
+
url = "#{glacier_on_rails.aws_create_archive_path}"
|
33
|
+
$.ajax
|
34
|
+
beforeSend : show_spinner
|
35
|
+
method : 'post'
|
36
|
+
url : url
|
37
|
+
success : add
|
38
|
+
complete : hide_spinner
|
39
|
+
show_spinner = (jqxhr, settings)->
|
40
|
+
$('a, .action_link').addClass('disabled')
|
41
|
+
$('body').append("<div id='scrim'></div><i id='spinner' class='fa fa-pulse fa-spinner fa-3x' />")
|
42
|
+
true
|
43
|
+
hide_spinner = ->
|
44
|
+
$('a').removeClass('disabled')
|
45
|
+
$('.action_link').removeClass('disabled')
|
46
|
+
$('#scrim').remove()
|
47
|
+
$('#spinner').remove()
|
48
|
+
add = (data, status, jqxhr)->
|
49
|
+
$('#application_data_backups').append(data)
|
50
|
+
@initiate_retrieval = (id)->
|
51
|
+
data = {application_data_backup_id : id}
|
52
|
+
url = "#{glacier_on_rails.aws_archive_retrieval_job_create_path}"
|
53
|
+
$.ajax
|
54
|
+
beforeSend : show_spinner
|
55
|
+
complete : hide_spinner
|
56
|
+
url : url
|
57
|
+
method : 'post'
|
58
|
+
data : data
|
59
|
+
context : $(event.target).closest('tr')
|
60
|
+
success : update
|
61
|
+
@fetch_archive = (id)->
|
62
|
+
url = "#{glacier_on_rails.aws_fetch_archive_path}"
|
63
|
+
data = {application_data_backup_id : id}
|
64
|
+
context = $(event.target).closest('tr')
|
65
|
+
$.ajax
|
66
|
+
beforeSend : show_spinner
|
67
|
+
complete : hide_spinner
|
68
|
+
method : 'post'
|
69
|
+
url : url
|
70
|
+
data : data
|
71
|
+
context : $(event.target).closest('tr')
|
72
|
+
success : update
|
73
|
+
update = (data, status, jqxhr)->
|
74
|
+
@replaceWith(data)
|
75
|
+
@restore = (id)->
|
76
|
+
url = "#{glacier_on_rails.aws_restore_archive_path}"
|
77
|
+
$.ajax
|
78
|
+
beforeSend : show_spinner
|
79
|
+
complete : hide_spinner
|
80
|
+
method : 'post'
|
81
|
+
url : url
|
82
|
+
data : {application_data_backup_id : id}
|
83
|
+
context : $(event.target).closest('tr')
|
84
|
+
success : restore_success #jquery handles js for error response
|
85
|
+
restore_success = (data, status, jqxhr)->
|
86
|
+
@replaceWith(data)
|
87
|
+
backup_date = $(data).find('.date').text()
|
88
|
+
flash.confirm('Database restored with the '+backup_date+' backup')
|
89
|
+
@delete_archive = (id)->
|
90
|
+
url = "#{glacier_on_rails.aws_destroy_archive_path}"
|
91
|
+
context = $(event.target).closest('tr')
|
92
|
+
$.ajax
|
93
|
+
beforeSend : show_spinner
|
94
|
+
complete : hide_spinner
|
95
|
+
method : 'delete'
|
96
|
+
url : url
|
97
|
+
data : {application_data_backup_id : id}
|
98
|
+
context : context
|
99
|
+
success : remove_archive
|
100
|
+
remove_archive = ->
|
101
|
+
@remove()
|
@@ -0,0 +1 @@
|
|
1
|
+
%td.restore_database.action_link{:onclick=>"restore(#{application_data_backup.id})"} restore
|
@@ -0,0 +1 @@
|
|
1
|
+
%td.retrieval_pending retrieval pending
|
@@ -0,0 +1 @@
|
|
1
|
+
AwsLog.logger = Logger.new(AwsLog::LogFile)
|
data/config/routes.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#Rails.application.routes.draw do
|
2
|
+
GlacierOnRails::Engine.routes.draw do
|
3
|
+
post :aws_subscription_notify, :to => 'aws_sns_subscriptions#create'
|
4
|
+
post :aws_archive_retrieval_job_create, :to => 'aws_archive_retrieval_jobs#create'
|
5
|
+
post :aws_fetch_archive, :to => 'application_data_backups#fetch'
|
6
|
+
delete :aws_destroy_archive, :to => 'application_data_backups#destroy'
|
7
|
+
post :aws_restore_archive, :to => 'application_data_backups#restore'
|
8
|
+
post :aws_create_archive, :to => 'application_data_backups#create'
|
9
|
+
end
|