dmitryv-backup 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +125 -0
- data/LICENSE +20 -0
- data/README.md +180 -0
- data/VERSION +1 -0
- data/bin/backup +108 -0
- data/generators/backup/backup_generator.rb +69 -0
- data/generators/backup/templates/backup.rake +56 -0
- data/generators/backup/templates/backup.rb +229 -0
- data/generators/backup/templates/create_backup_tables.rb +18 -0
- data/generators/backup_update/backup_update_generator.rb +50 -0
- data/generators/backup_update/templates/migrations/update_backup_tables.rb +27 -0
- data/lib/backup.rb +131 -0
- data/lib/backup/adapters/archive.rb +34 -0
- data/lib/backup/adapters/base.rb +138 -0
- data/lib/backup/adapters/custom.rb +41 -0
- data/lib/backup/adapters/mysql.rb +60 -0
- data/lib/backup/adapters/postgresql.rb +56 -0
- data/lib/backup/adapters/sqlite.rb +25 -0
- data/lib/backup/command_helper.rb +11 -0
- data/lib/backup/compressors/base.rb +7 -0
- data/lib/backup/compressors/gzip.rb +9 -0
- data/lib/backup/compressors/seven_zip.rb +9 -0
- data/lib/backup/configuration/adapter.rb +21 -0
- data/lib/backup/configuration/adapter_options.rb +8 -0
- data/lib/backup/configuration/attributes.rb +19 -0
- data/lib/backup/configuration/base.rb +77 -0
- data/lib/backup/configuration/helpers.rb +24 -0
- data/lib/backup/configuration/mail.rb +20 -0
- data/lib/backup/configuration/smtp.rb +8 -0
- data/lib/backup/configuration/storage.rb +8 -0
- data/lib/backup/connection/cloudfiles.rb +75 -0
- data/lib/backup/connection/s3.rb +85 -0
- data/lib/backup/environment/base.rb +12 -0
- data/lib/backup/environment/rails_configuration.rb +15 -0
- data/lib/backup/environment/unix_configuration.rb +109 -0
- data/lib/backup/mail/base.rb +97 -0
- data/lib/backup/mail/mail.txt +7 -0
- data/lib/backup/record/base.rb +65 -0
- data/lib/backup/record/cloudfiles.rb +28 -0
- data/lib/backup/record/ftp.rb +39 -0
- data/lib/backup/record/local.rb +26 -0
- data/lib/backup/record/s3.rb +26 -0
- data/lib/backup/record/scp.rb +33 -0
- data/lib/backup/record/sftp.rb +38 -0
- data/lib/backup/storage/base.rb +10 -0
- data/lib/backup/storage/cloudfiles.rb +16 -0
- data/lib/backup/storage/ftp.rb +38 -0
- data/lib/backup/storage/local.rb +22 -0
- data/lib/backup/storage/s3.rb +17 -0
- data/lib/backup/storage/scp.rb +30 -0
- data/lib/backup/storage/sftp.rb +31 -0
- data/lib/generators/backup/USAGE +10 -0
- data/lib/generators/backup/backup_generator.rb +47 -0
- data/lib/generators/backup/templates/backup.rake +56 -0
- data/lib/generators/backup/templates/backup.rb +229 -0
- data/lib/generators/backup/templates/create_backup_tables.rb +18 -0
- data/setup/backup.rb +231 -0
- data/setup/backup.sqlite3 +0 -0
- metadata +271 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module Backup
|
2
|
+
module Adapters
|
3
|
+
class Archive < Backup::Adapters::Base
|
4
|
+
|
5
|
+
attr_accessor :files, :exclude
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Archives and Compresses all files
|
10
|
+
def perform
|
11
|
+
log system_messages[:archiving]; log system_messages[:compressing]
|
12
|
+
run "tar -czf #{File.join(tmp_path, compressed_file)} #{exclude_files} #{tar_files}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_settings
|
16
|
+
self.files = procedure.get_adapter_configuration.attributes['files']
|
17
|
+
self.exclude = procedure.get_adapter_configuration.attributes['exclude']
|
18
|
+
end
|
19
|
+
|
20
|
+
def performed_file_extension
|
21
|
+
".tar"
|
22
|
+
end
|
23
|
+
|
24
|
+
def tar_files
|
25
|
+
[*files].map{|f| f.gsub(' ', '\ ')}.join(' ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def exclude_files
|
29
|
+
[*exclude].compact.map{|x| "--exclude=#{x}"}.join(' ')
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Backup
|
2
|
+
module Adapters
|
3
|
+
class Base
|
4
|
+
|
5
|
+
include Backup::CommandHelper
|
6
|
+
|
7
|
+
attr_accessor :procedure, :timestamp, :options, :tmp_path, :encrypt_with_password, :encrypt_with_gpg_public_key, :keep_backups, :trigger
|
8
|
+
|
9
|
+
# IMPORTANT
|
10
|
+
# final_file must have the value of the final filename result
|
11
|
+
# so if a file gets compressed, then the file could look like this:
|
12
|
+
# myfile.gz
|
13
|
+
#
|
14
|
+
# and if a file afterwards gets encrypted, the file will look like:
|
15
|
+
# myfile.gz.enc (with a password)
|
16
|
+
# myfile.gz.gpg (with a gpg public key)
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# It is important that, whatever the final filename of the file will be, that :final_file will contain it.
|
20
|
+
attr_accessor :performed_file, :compressed_file, :encrypted_file, :final_file
|
21
|
+
|
22
|
+
# Initializes the Backup Process
|
23
|
+
#
|
24
|
+
# This will first load in any prefixed settings from the Backup::Adapters::Base
|
25
|
+
# Then it will add it's own settings.
|
26
|
+
#
|
27
|
+
# First it will call the 'perform' method. This method is concerned with the backup, and must
|
28
|
+
# be implemented by derived classes!
|
29
|
+
# Then it will optionally encrypt the backed up file
|
30
|
+
# Then it will store it to the specified storage location
|
31
|
+
# Then it will record the data to the database
|
32
|
+
# Once this is all done, all the temporary files will be removed
|
33
|
+
#
|
34
|
+
# Wrapped inside of begin/ensure/end block to ensure the deletion of any files in the tmp directory
|
35
|
+
def initialize(trigger, procedure)
|
36
|
+
self.trigger = trigger
|
37
|
+
self.procedure = procedure
|
38
|
+
self.timestamp = Time.now.strftime("%Y%m%d%H%M%S")
|
39
|
+
self.tmp_path = File.join(BACKUP_PATH.gsub(' ', '\ '), 'tmp', 'backup', trigger)
|
40
|
+
self.encrypt_with_password = procedure.attributes['encrypt_with_password']
|
41
|
+
self.encrypt_with_gpg_public_key = procedure.attributes['encrypt_with_gpg_public_key']
|
42
|
+
self.keep_backups = procedure.attributes['keep_backups']
|
43
|
+
|
44
|
+
self.performed_file = "#{timestamp}.#{trigger.gsub(' ', '-')}#{performed_file_extension}"
|
45
|
+
self.compressed_file = "#{performed_file}.#{procedure.compressor_class::ARCHIVE_EXTENSION}"
|
46
|
+
self.final_file = compressed_file
|
47
|
+
|
48
|
+
begin
|
49
|
+
create_tmp_folder
|
50
|
+
load_settings # if respond_to?(:load_settings)
|
51
|
+
perform
|
52
|
+
encrypt
|
53
|
+
store
|
54
|
+
record
|
55
|
+
notify
|
56
|
+
ensure
|
57
|
+
remove_tmp_files
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates the temporary folder for the specified adapter
|
62
|
+
def create_tmp_folder
|
63
|
+
run "mkdir -p #{tmp_path}"
|
64
|
+
end
|
65
|
+
|
66
|
+
# TODO make methods in derived classes public? respond_to cannot identify private methods
|
67
|
+
def load_settings
|
68
|
+
end
|
69
|
+
|
70
|
+
# Removes the files inside the temporary folder
|
71
|
+
def remove_tmp_files
|
72
|
+
run "rm #{File.join(tmp_path, '*')}"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Encrypts the archive file
|
76
|
+
def encrypt
|
77
|
+
if encrypt_with_gpg_public_key.is_a?(String) && encrypt_with_password.is_a?(String)
|
78
|
+
puts "both 'encrypt_with_gpg_public_key' and 'encrypt_with_password' are set. Please choose one or the other. Exiting."
|
79
|
+
exit 1
|
80
|
+
end
|
81
|
+
|
82
|
+
if encrypt_with_gpg_public_key.is_a?(String)
|
83
|
+
if `which gpg` == ''
|
84
|
+
puts "Encrypting with a GPG public key requires that gpg be in your public path. gpg was not found. Exiting"
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
log system_messages[:encrypting_w_key]
|
88
|
+
self.encrypted_file = "#{self.final_file}.gpg"
|
89
|
+
|
90
|
+
# tmp_file = Tempfile.new('backup.pub'){ |tmp_file| tmp_file << encrypt_with_gpg_public_key }
|
91
|
+
tmp_file = Tempfile.new('backup.pub')
|
92
|
+
tmp_file << encrypt_with_gpg_public_key
|
93
|
+
tmp_file.close
|
94
|
+
# that will either say the key was added OR that it wasn't needed, but either way we need to parse for the uid
|
95
|
+
# which will be wrapped in '<' and '>' like <sweetspot-backup2007@6bar8.com>
|
96
|
+
encryptionKeyId = `gpg --import #{tmp_file.path} 2>&1`.match(/<(.+)>/)[1]
|
97
|
+
run "gpg -e --trust-model always -o #{File.join(tmp_path, encrypted_file)} -r '#{encryptionKeyId}' #{File.join(tmp_path, compressed_file)}"
|
98
|
+
elsif encrypt_with_password.is_a?(String)
|
99
|
+
log system_messages[:encrypting_w_pass]
|
100
|
+
self.encrypted_file = "#{self.final_file}.enc"
|
101
|
+
run "openssl enc -des-cbc -in #{File.join(tmp_path, compressed_file)} -out #{File.join(tmp_path, encrypted_file)} -k #{encrypt_with_password}"
|
102
|
+
end
|
103
|
+
self.final_file = encrypted_file if encrypted_file
|
104
|
+
end
|
105
|
+
|
106
|
+
# Initializes the storing process
|
107
|
+
def store
|
108
|
+
procedure.initialize_storage(self)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Records data on every individual file to the database
|
112
|
+
def record
|
113
|
+
record = procedure.initialize_record
|
114
|
+
record.load_adapter(self)
|
115
|
+
record.save
|
116
|
+
end
|
117
|
+
|
118
|
+
# Delivers a notification by email regarding the successfully stored backup
|
119
|
+
def notify
|
120
|
+
if Backup::Mail::Base.setup?
|
121
|
+
Backup::Mail::Base.notify!(self)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def system_messages
|
126
|
+
{ :compressing => "Compressing backup..",
|
127
|
+
:archiving => "Archiving backup..",
|
128
|
+
:encrypting_w_pass => "Encrypting backup with password..",
|
129
|
+
:encrypting_w_key => "Encrypting backup with gpg public key..",
|
130
|
+
:mysqldump => "Creating MySQL dump..",
|
131
|
+
:pgdump => "Creating PostgreSQL dump..",
|
132
|
+
:sqlite => "Copying and compressing SQLite database..",
|
133
|
+
:commands => "Executing commands.." }
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Backup
|
2
|
+
module Adapters
|
3
|
+
class Custom < Backup::Adapters::Base
|
4
|
+
|
5
|
+
attr_accessor :commands
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Execute any given commands, then archive and compress every folder/file
|
10
|
+
def perform
|
11
|
+
execute_commands
|
12
|
+
targz
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes the commands
|
16
|
+
def execute_commands
|
17
|
+
return unless commands
|
18
|
+
log system_messages[:commands]
|
19
|
+
[*commands].each do |command|
|
20
|
+
run "#{command.gsub(':tmp_path', tmp_path)}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Archives and Compresses
|
25
|
+
def targz
|
26
|
+
log system_messages[:archiving]; log system_messages[:compressing]
|
27
|
+
run "tar -czf #{File.join(tmp_path, compressed_file)} #{File.join(tmp_path, '*')}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def performed_file_extension
|
31
|
+
".tar"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Loads the initial settings
|
35
|
+
def load_settings
|
36
|
+
self.commands = procedure.get_adapter_configuration.attributes['commands']
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Backup
|
2
|
+
module Adapters
|
3
|
+
class MySQL < Backup::Adapters::Base
|
4
|
+
|
5
|
+
attr_accessor :user, :password, :database, :skip_tables, :host, :port, :socket, :additional_options, :tables
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Dumps and Compresses the MySQL file
|
10
|
+
def perform
|
11
|
+
log system_messages[:mysqldump]; log system_messages[:compressing]
|
12
|
+
run "#{mysqldump} -u #{user} --password='#{password}' #{options} #{additional_options} #{database} #{tables_to_include} #{tables_to_skip} | #{procedure.compressor_class::COMPRESS_COMMAND} #{File.join(tmp_path, compressed_file)}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def mysqldump
|
16
|
+
# try to determine the full path, and fall back to myqsldump if not found
|
17
|
+
cmd = `which mysqldump`.chomp
|
18
|
+
cmd = 'mysqldump' if cmd.empty?
|
19
|
+
cmd
|
20
|
+
end
|
21
|
+
|
22
|
+
def performed_file_extension
|
23
|
+
".sql"
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads the initial settings
|
27
|
+
def load_settings
|
28
|
+
%w(user password database tables skip_tables additional_options).each do |attribute|
|
29
|
+
send(:"#{attribute}=", procedure.get_adapter_configuration.attributes[attribute])
|
30
|
+
end
|
31
|
+
|
32
|
+
%w(host port socket).each do |attribute|
|
33
|
+
send(:"#{attribute}=", procedure.get_adapter_configuration.get_options.attributes[attribute])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a list of options in MySQL syntax
|
38
|
+
def options
|
39
|
+
options = String.new
|
40
|
+
options += " --host='#{host}' " unless host.blank?
|
41
|
+
options += " --port='#{port}' " unless port.blank?
|
42
|
+
options += " --socket='#{socket}' " unless socket.blank?
|
43
|
+
options
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a list of tables to skip in MySQL syntax
|
47
|
+
def tables_to_skip
|
48
|
+
return "" unless skip_tables
|
49
|
+
[*skip_tables].map {|table| " --ignore-table='#{database}.#{table}' "}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a list of tables to include in MySQL syntax
|
53
|
+
def tables_to_include
|
54
|
+
return "" unless tables
|
55
|
+
[*tables].join(" ")
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Backup
|
2
|
+
module Adapters
|
3
|
+
class PostgreSQL < Backup::Adapters::Base
|
4
|
+
|
5
|
+
attr_accessor :user, :password, :database, :skip_tables, :host, :port, :socket, :additional_options
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Dumps and Compresses the PostgreSQL file
|
10
|
+
def perform
|
11
|
+
log system_messages[:pgdump]; log system_messages[:compressing]
|
12
|
+
ENV['PGPASSWORD'] = password
|
13
|
+
run "#{pg_dump} -U #{user} #{options} #{additional_options} #{tables_to_skip} #{database} | gzip -f --best > #{File.join(tmp_path, compressed_file)}"
|
14
|
+
ENV['PGPASSWORD'] = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def pg_dump
|
18
|
+
# try to determine the full path, and fall back to pg_dump if not found
|
19
|
+
cmd = `which pg_dump`.chomp
|
20
|
+
cmd = 'pg_dump' if cmd.empty?
|
21
|
+
cmd
|
22
|
+
end
|
23
|
+
|
24
|
+
def performed_file_extension
|
25
|
+
".sql"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Loads the initial settings
|
29
|
+
def load_settings
|
30
|
+
%w(user password database skip_tables additional_options).each do |attribute|
|
31
|
+
send(:"#{attribute}=", procedure.get_adapter_configuration.attributes[attribute])
|
32
|
+
end
|
33
|
+
|
34
|
+
%w(host port socket).each do |attribute|
|
35
|
+
send(:"#{attribute}=", procedure.get_adapter_configuration.get_options.attributes[attribute])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a list of options in PostgreSQL syntax
|
40
|
+
def options
|
41
|
+
options = String.new
|
42
|
+
options += " --port='#{port}' " unless port.blank?
|
43
|
+
options += " --host='#{host}' " unless host.blank?
|
44
|
+
options += " --host='#{socket}' " unless socket.blank? unless options.include?('--host=')
|
45
|
+
options
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns a list of tables to skip in PostgreSQL syntax
|
49
|
+
def tables_to_skip
|
50
|
+
return "" unless skip_tables
|
51
|
+
[*skip_tables].map {|table| " -T \"#{table}\" "}
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Backup
|
2
|
+
module Adapters
|
3
|
+
class SQLite < Base
|
4
|
+
|
5
|
+
attr_accessor :database
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Compress the sqlite file
|
10
|
+
def perform
|
11
|
+
log system_messages[:sqlite]
|
12
|
+
run "gzip -c --best #{database} > #{File.join(tmp_path, compressed_file)}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_settings
|
16
|
+
self.database = procedure.get_adapter_configuration.attributes['database']
|
17
|
+
end
|
18
|
+
|
19
|
+
def performed_file_extension
|
20
|
+
""
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Backup
|
2
|
+
module Configuration
|
3
|
+
class Adapter
|
4
|
+
extend Backup::Configuration::Attributes
|
5
|
+
generate_attributes %w(files exclude user password database tables skip_tables commands additional_options)
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@options = Backup::Configuration::AdapterOptions.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def options(&block)
|
12
|
+
@options.instance_eval &block
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_options
|
16
|
+
@options
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Backup
|
2
|
+
module Configuration
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
def generate_attributes(*attrs)
|
6
|
+
define_method :attributes do
|
7
|
+
@attributes ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
attrs.flatten.each do |att|
|
11
|
+
define_method att do |value|
|
12
|
+
self.attributes[att.to_s] = value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|