backup-agent 1.0.9 → 2.0.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.
- checksums.yaml +4 -4
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +4 -20
- data/backup-agent.gemspec +16 -14
- data/lib/backup-agent.rb +30 -15
- data/lib/backup-agent/credentials.rb +36 -0
- data/lib/backup-agent/dsl.rb +104 -0
- data/lib/backup-agent/performer.rb +16 -106
- data/lib/backup-agent/storages.rb +42 -0
- data/lib/backup-agent/storages/amazon-s3.rb +51 -0
- data/lib/backup-agent/storages/base.rb +38 -0
- data/lib/backup-agent/storages/local.rb +37 -0
- data/lib/backup-agent/tasks/directory.rb +61 -0
- data/lib/backup-agent/tasks/mysql.rb +84 -0
- data/lib/backup-agent/version.rb +6 -0
- data/test.rb +20 -0
- metadata +33 -27
- data/Gemfile +0 -4
- data/Rakefile +0 -1
- data/lib/backup-agent/abstract_storage.rb +0 -32
- data/lib/backup-agent/abstract_storage_config.rb +0 -5
- data/lib/backup-agent/abstract_storage_object.rb +0 -19
- data/lib/backup-agent/features.rb +0 -30
- data/lib/backup-agent/s3_config.rb +0 -11
- data/lib/backup-agent/s3_object.rb +0 -17
- data/lib/backup-agent/s3_storage.rb +0 -45
- data/lib/backup-agent/task.rb +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f391904e3b281dd04dc9b8f5983c56a9da2f387
|
4
|
+
data.tar.gz: 774815f802892ee300d8e5dc87e34f3f7fe8bfee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8dbb5c1a4593f3dfd7b928a655a7c13f8c4b6ad9a60a3b40838e1af04583b034305d90ffbc2434ae6eaecb793c2e252998d3ffd311d7975d8ee2a994887a446
|
7
|
+
data.tar.gz: aeef30268dae8a1955d4045e8a195a20efa31c4fb70cfeacc68770d06f0c1feeb3ac9bd4bbc89b6b67787a1ea34de0593dca44181455039be4f463e782488369
|
data/{LICENSE.txt → LICENSE}
RENAMED
data/README.md
CHANGED
@@ -1,20 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
set :access_key_id, 'xxx'
|
6
|
-
set :secret_access_key, 'yyy'
|
7
|
-
set :region, 'eu-central-1'
|
8
|
-
end
|
9
|
-
|
10
|
-
storage = Backup::S3Storage.new(storage_config, bucket: 'my-backups')
|
11
|
-
|
12
|
-
Backup.perform storage do
|
13
|
-
set :mysql_host, 'xxx.yyy.xxx.yyy'
|
14
|
-
end
|
15
|
-
```
|
16
|
-
|
17
|
-
## Gemfile
|
18
|
-
```ruby
|
19
|
-
gem 'backup-agent'
|
20
|
-
```
|
1
|
+
### TODO:
|
2
|
+
* Update description in gemspec
|
3
|
+
* Add ability to specify name with slashes in directory backup task
|
4
|
+
* Release v2.1
|
data/backup-agent.gemspec
CHANGED
@@ -1,21 +1,23 @@
|
|
1
|
-
# encoding:
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require File.expand_path("../lib/backup-agent/version", __FILE__)
|
2
5
|
|
3
6
|
Gem::Specification.new do |s|
|
4
|
-
s.name =
|
5
|
-
s.version =
|
6
|
-
s.authors = [
|
7
|
-
s.email = [
|
8
|
-
s.summary =
|
9
|
-
s.description =
|
10
|
-
s.homepage =
|
11
|
-
s.license =
|
7
|
+
s.name = "backup-agent"
|
8
|
+
s.version = Backup::VERSION
|
9
|
+
s.authors = ["Yaroslav Konoplov"]
|
10
|
+
s.email = ["eahome00@gmail.com"]
|
11
|
+
s.summary = "Easy AWS S3 backup"
|
12
|
+
s.description = "Easy AWS S3 backup"
|
13
|
+
s.homepage = "http://github.com/yivo/backup-agent"
|
14
|
+
s.license = "MIT"
|
12
15
|
|
13
|
-
s.executables = `git ls-files -z -- bin/*`.split("\x0").map{ |f| File.basename(f) }
|
14
16
|
s.files = `git ls-files -z`.split("\x0")
|
15
17
|
s.test_files = `git ls-files -z -- {test,spec,features}/*`.split("\x0")
|
16
|
-
s.require_paths = [
|
18
|
+
s.require_paths = ["lib"]
|
17
19
|
|
18
|
-
s.add_dependency
|
19
|
-
s.add_dependency
|
20
|
-
s.add_dependency
|
20
|
+
s.add_dependency "aws-sdk", "~> 2"
|
21
|
+
s.add_dependency "activesupport", ">= 3.0", "< 6.0"
|
22
|
+
s.add_dependency "method-not-implemented", "~> 1.0"
|
21
23
|
end
|
data/lib/backup-agent.rb
CHANGED
@@ -1,19 +1,34 @@
|
|
1
|
-
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
|
4
|
+
require "fileutils"
|
5
|
+
require "tempfile"
|
6
|
+
require "shellwords"
|
7
|
+
require "open3"
|
8
|
+
require "singleton"
|
9
|
+
require "aws-sdk"
|
10
|
+
require "method-not-implemented"
|
11
|
+
require "active_support/core_ext/object/blank"
|
12
|
+
require "active_support/core_ext/string/filters"
|
13
|
+
require "active_support/core_ext/string/multibyte"
|
14
|
+
require "active_support/core_ext/numeric/time"
|
4
15
|
|
5
|
-
|
6
|
-
|
7
|
-
|
16
|
+
["ruby", "tar", "gzip", "xz", "mysql", "mysqldump"].each do |x|
|
17
|
+
puts Open3.capture3(x, "--version")[0...2].map(&:squish).reject(&:blank?).join(' ')
|
18
|
+
end
|
8
19
|
|
9
|
-
|
10
|
-
class << self
|
11
|
-
def perform(storage, &block)
|
12
|
-
Performer.new.perform_backup(storage, Task.new(&block))
|
13
|
-
end
|
20
|
+
$LOAD_PATH << __dir__ unless $LOAD_PATH.include?(__dir__)
|
14
21
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
22
|
+
require "backup-agent/dsl"
|
23
|
+
require "backup-agent/credentials"
|
24
|
+
require "backup-agent/performer"
|
25
|
+
|
26
|
+
require "backup-agent/storages"
|
27
|
+
require "backup-agent/storages/base"
|
28
|
+
require "backup-agent/storages/local"
|
29
|
+
require "backup-agent/storages/amazon-s3"
|
30
|
+
|
31
|
+
require "backup-agent/tasks/directory"
|
32
|
+
require "backup-agent/tasks/mysql"
|
33
|
+
|
34
|
+
include Backup::DSL
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Backup
|
5
|
+
class Credentials
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@groups = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# Usage: credentials(type: :name)
|
13
|
+
def [](pair)
|
14
|
+
@groups.fetch(pair.keys[0]).fetch(pair.values[0])
|
15
|
+
end
|
16
|
+
|
17
|
+
# define Class => [:type, :name, arguments...]
|
18
|
+
def define(definitions)
|
19
|
+
definitions.map do |klass, definition|
|
20
|
+
(@groups[definition[0]] ||= {})[definition[1]] = klass.new(*definition.drop(2))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def mysql(definitions)
|
25
|
+
definitions.map do |name, args|
|
26
|
+
define Backup::Tasks::MySQL::Credentials => [:mysql, name, *[args].flatten(1)]
|
27
|
+
end.flatten(1)
|
28
|
+
end
|
29
|
+
|
30
|
+
def amazon_s3(definitions)
|
31
|
+
definitions.map do |name, args|
|
32
|
+
define Backup::Storages::AmazonS3::Credentials => [:amazon_s3, name, *[args].flatten(1)]
|
33
|
+
end.flatten(1)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Backup
|
5
|
+
module DSL
|
6
|
+
def echo(*args)
|
7
|
+
puts(*args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def with(environment)
|
11
|
+
@current_command_environment = environment&.each_with_object({}) { |(k, v), m| m[k.to_s] = v.to_s }
|
12
|
+
yield
|
13
|
+
ensure
|
14
|
+
remove_instance_variable(:@current_command_environment)
|
15
|
+
end
|
16
|
+
|
17
|
+
def stdin(data, binmode: false)
|
18
|
+
@current_command_stdin_data = data
|
19
|
+
@current_command_stdin_data_binmode = binmode
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
remove_instance_variable(:@current_command_stdin_data)
|
23
|
+
remove_instance_variable(:@current_command_stdin_data_binmode)
|
24
|
+
end
|
25
|
+
|
26
|
+
def command(*args)
|
27
|
+
returned, msec = measure args.map(&:to_s).join(" ") do
|
28
|
+
|
29
|
+
if instance_variable_defined?(:@current_command_environment) && @current_command_environment
|
30
|
+
args.unshift(@current_command_environment)
|
31
|
+
end
|
32
|
+
|
33
|
+
stdout, stderr, exit_status = \
|
34
|
+
if instance_variable_defined?(:@current_command_stdin_data)
|
35
|
+
Open3.capture3 *args, \
|
36
|
+
stdin_data: @current_command_stdin_data,
|
37
|
+
binmode: @current_command_stdin_data_binmode
|
38
|
+
else
|
39
|
+
Open3.capture3(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
fail stderr unless exit_status.success?
|
43
|
+
# echo stdout
|
44
|
+
stdout
|
45
|
+
end
|
46
|
+
returned
|
47
|
+
end
|
48
|
+
|
49
|
+
def measure(action)
|
50
|
+
echo "\n", action
|
51
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
52
|
+
returned = yield
|
53
|
+
finished = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
54
|
+
echo "(#{ (finished - started).round(1) }ms)", "\n"
|
55
|
+
returned
|
56
|
+
end
|
57
|
+
|
58
|
+
def construct_filename(basename, extension_with_dot = nil)
|
59
|
+
[basename.gsub(/[^[[:alnum:]]]/i, "-")
|
60
|
+
.gsub(/[-–—]+/, "-")
|
61
|
+
.mb_chars.downcase.to_s,
|
62
|
+
"--#{Time.now.getutc.strftime("%Y-%m-%d--%H-%M-%S--UTC")}",
|
63
|
+
extension_with_dot.to_s.mb_chars.downcase.to_s].join("")
|
64
|
+
end
|
65
|
+
|
66
|
+
def storages(pair = nil, &block)
|
67
|
+
if pair
|
68
|
+
Backup::Storages.instance[pair]
|
69
|
+
elsif block
|
70
|
+
Backup::Storages.instance.instance_exec(&block)
|
71
|
+
else
|
72
|
+
Backup::Storages.instance
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def credentials(pair = nil, &block)
|
77
|
+
if pair
|
78
|
+
Backup::Credentials.instance[pair]
|
79
|
+
elsif block
|
80
|
+
Backup::Credentials.instance.instance_exec(&block)
|
81
|
+
else
|
82
|
+
Backup::Credentials.instance
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def backup(to:, &block)
|
87
|
+
storages = [to].flatten.map { |h| h.map { |k, v| self.storages(k => v) } }.flatten
|
88
|
+
Backup::Performer.new(storages).tap { |performer| performer.instance_eval(&block) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def delete_backups_older_than(x)
|
92
|
+
cutoff_timestamp = Time.now.utc.to_i - x
|
93
|
+
storages.each do |storage|
|
94
|
+
storage.each do |object|
|
95
|
+
if object.last_modified.to_i < cutoff_timestamp
|
96
|
+
puts "Delete #{object.to_s} from #{storage.to_s}"
|
97
|
+
storage.delete(object.id)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
@@ -1,119 +1,29 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
1
4
|
module Backup
|
2
5
|
class Performer
|
3
|
-
|
4
|
-
|
5
|
-
def perform_backup(storage, config)
|
6
|
-
@storage = storage
|
7
|
-
@config = config
|
8
|
-
@started_at = Time.now.utc
|
9
|
-
@timestamp = @started_at.strftime('%s - %A %d %B %Y %H:%M')
|
10
|
-
storage.open
|
11
|
-
make_tmp_dir
|
12
|
-
backup_mysql if Backup.features.mysql_installed?
|
13
|
-
backup_mongodb if Backup.features.mongodb_installed?
|
14
|
-
backup_directories
|
15
|
-
backup_files
|
16
|
-
cleanup_old_backups
|
17
|
-
ensure
|
18
|
-
remove_tmp_dir
|
19
|
-
storage.close
|
20
|
-
end
|
21
|
-
|
22
|
-
protected
|
23
|
-
|
24
|
-
def backup_directories
|
25
|
-
config.get(:directories).each do |name, dir|
|
26
|
-
dir_filename = "#{name}.tar.xz"
|
27
|
-
dir_fileparam = Shellwords.escape(dir_filename)
|
28
|
-
cmd = "cd #{dir} && /usr/bin/env XZ_OPT=-9 tar -cJvf #{tmp_path}/#{dir_fileparam} ."
|
29
|
-
puts "Exec #{cmd}"
|
30
|
-
system(cmd)
|
31
|
-
storage.upload("#{@timestamp}/#{dir_filename}", "#{tmp_path}/#{dir_filename}")
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def backup_files
|
36
|
-
config.get(:files).each do |name, files|
|
37
|
-
begin
|
38
|
-
files_tmp_path = File.join(tmp_path, "#{name}-tmp")
|
39
|
-
file_bunch_name = "#{name}.tar.xz"
|
40
|
-
file_bunch_param = Shellwords.escape(file_bunch_name)
|
41
|
-
|
42
|
-
FileUtils.mkdir_p(files_tmp_path)
|
43
|
-
FileUtils.cp(files.select { |el| File.exists?(el) }, files_tmp_path)
|
44
|
-
|
45
|
-
cmd = "cd #{files_tmp_path} && /usr/bin/env XZ_OPT=-3 tar -cJvf #{tmp_path}/#{file_bunch_param} ."
|
46
|
-
system(cmd)
|
47
|
-
|
48
|
-
storage.upload("#{@timestamp}/#{file_bunch_name}", "#{tmp_path}/#{file_bunch_name}")
|
49
|
-
ensure
|
50
|
-
FileUtils.remove_dir(files_tmp_path)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def backup_mysql
|
56
|
-
config.get(:mysql_databases).each do |db|
|
57
|
-
dump_path = "#{tmp_path}/#{ shell_escape(db) }.sql"
|
58
|
-
dump_cmd = "mysqldump #{config.get(:mysql_connect)} #{config.get(:mysqldump_options).join(' ')} --databases #{db}"
|
59
|
-
|
60
|
-
exec with_env("#{dump_cmd} > #{dump_path}")
|
61
|
-
exec with_env("xz --compress -9 --keep --threads=0 --verbose #{dump_path}")
|
62
|
-
|
63
|
-
storage.upload("#{@timestamp}/#{ shell_escape(db) }.sql.xz", "#{dump_path}.xz")
|
64
|
-
end
|
6
|
+
def initialize(storages)
|
7
|
+
@storages = storages
|
65
8
|
end
|
66
9
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
config.get(:mongo_databases).each do |db|
|
72
|
-
db_filename = "Mongo Database #{db}.tar.xz"
|
73
|
-
db_fileparam = Shellwords.escape(db_filename)
|
74
|
-
dump = with_env "mongodump #{config.get(:mongo_connect)} -d #{db} -o #{mongo_dump_dir}"
|
75
|
-
cd = "cd #{mongo_dump_dir}/#{db}"
|
76
|
-
tar = with_env "XZ_OPT=-9 tar -cJvf #{tmp_path}/#{db_fileparam} ."
|
77
|
-
|
78
|
-
puts "Exec #{dump} && #{cd} && #{tar}"
|
79
|
-
system "#{dump} && #{cd} && #{tar}"
|
80
|
-
|
81
|
-
storage.upload("#{@timestamp}/#{db_filename}", "#{tmp_path}/#{db_filename}")
|
10
|
+
# task Task => [:foo, :bar, :baz]
|
11
|
+
def task(arg)
|
12
|
+
arg.each do |klass, args|
|
13
|
+
@storages.each { |storage| klass.new(*args).perform(storage) }
|
82
14
|
end
|
83
|
-
|
84
|
-
FileUtils.remove_dir(mongo_dump_dir)
|
15
|
+
nil
|
85
16
|
end
|
86
17
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
90
|
-
obj.delete if obj.last_modified.to_i < cutoff_date
|
18
|
+
def mysql(options)
|
19
|
+
if Symbol === options[:credentials]
|
20
|
+
options[:credentials] = credentials(mysql: options[:credentials])
|
91
21
|
end
|
22
|
+
task Backup::Tasks::MySQL => [options]
|
92
23
|
end
|
93
24
|
|
94
|
-
def
|
95
|
-
|
96
|
-
end
|
97
|
-
|
98
|
-
def remove_tmp_dir
|
99
|
-
FileUtils.remove_dir(tmp_path)
|
100
|
-
end
|
101
|
-
|
102
|
-
def tmp_path
|
103
|
-
"/tmp/backup-agent-#{@started_at.strftime('%d-%m-%Y-%H:%M')}"
|
104
|
-
end
|
105
|
-
|
106
|
-
def with_env(cmd)
|
107
|
-
"/usr/bin/env #{cmd}"
|
108
|
-
end
|
109
|
-
|
110
|
-
def shell_escape(x)
|
111
|
-
Shellwords.escape(x)
|
112
|
-
end
|
113
|
-
|
114
|
-
def exec(cmd)
|
115
|
-
puts "Exec #{cmd}"
|
116
|
-
system cmd
|
25
|
+
def directory(path, options = {})
|
26
|
+
task Backup::Tasks::Directory => [path, options]
|
117
27
|
end
|
118
28
|
end
|
119
29
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Backup
|
5
|
+
class Storages
|
6
|
+
include Singleton
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@groups = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](pair)
|
14
|
+
@groups.fetch(pair.keys[0]).fetch(pair.values[0])
|
15
|
+
end
|
16
|
+
|
17
|
+
def each
|
18
|
+
@groups.each do |type, storages|
|
19
|
+
storages.each { |name, storage| yield storage, type, name }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# register AmazonS3 => [:amazon_s3, :default, storage constructor arguments...]
|
24
|
+
def register(arg)
|
25
|
+
arg.map do |klass, rest|
|
26
|
+
(@groups[rest[0]] ||= {})[rest[1]] = klass.new(*rest.drop(2))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def local(definitions)
|
31
|
+
definitions.map do |name, args|
|
32
|
+
register Backup::Storages::Local => [:local, name, *[args].flatten(1)]
|
33
|
+
end.flatten(1)
|
34
|
+
end
|
35
|
+
|
36
|
+
def amazon_s3(definitions)
|
37
|
+
definitions.map do |name, options|
|
38
|
+
register Backup::Storages::AmazonS3 => [:amazon_s3, name, options.merge(credentials: credentials[amazon_s3: name])]
|
39
|
+
end.flatten(1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# Storage based on Amazon S3
|
6
|
+
#
|
7
|
+
class Backup::Storages
|
8
|
+
class AmazonS3 < Backup::Storages::Base
|
9
|
+
def initialize(region:, bucket:, credentials:)
|
10
|
+
@aws_s3_ruby_credentials = Aws::Credentials.new(credentials.access_key_id, credentials.secret_access_key)
|
11
|
+
@aws_s3_ruby_resource = Aws::S3::Resource.new(region: region, credentials: @aws_s3_ruby_credentials)
|
12
|
+
@aws_s3_ruby_bucket = @aws_s3_ruby_resource.bucket(bucket)
|
13
|
+
end
|
14
|
+
|
15
|
+
def store(id, file_to_upload)
|
16
|
+
aws_s3_ruby_object = @aws_s3_ruby_bucket.object(id)
|
17
|
+
aws_s3_ruby_object.upload_file(file_to_upload)
|
18
|
+
Backup::Storages::AmazonS3::Object.new(self, @aws_s3_ruby_bucket, aws_s3_ruby_object.key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(id)
|
22
|
+
@aws_s3_ruby_bucket.object(id).delete
|
23
|
+
end
|
24
|
+
|
25
|
+
def each
|
26
|
+
@aws_s3_ruby_bucket.objects.each do |aws_s3_ruby_object|
|
27
|
+
yield Backup::Storages::AmazonS3::Object.new(self, @aws_s3_ruby_bucket, aws_s3_ruby_object.key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Credentials
|
32
|
+
attr_reader :access_key_id, :secret_access_key
|
33
|
+
|
34
|
+
def initialize(access_key_id:, secret_access_key:)
|
35
|
+
@access_key_id = access_key_id
|
36
|
+
@secret_access_key = secret_access_key
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Object < Backup::Storages::Base::Object
|
41
|
+
def initialize(storage, aws_s3_ruby_bucket, id)
|
42
|
+
super(storage, id)
|
43
|
+
@aws_s3_ruby_bucket = aws_s3_ruby_bucket
|
44
|
+
end
|
45
|
+
|
46
|
+
def last_modified
|
47
|
+
@aws_s3_ruby_bucket.object(id).last_modified.utc
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# Base stuff for storages
|
6
|
+
#
|
7
|
+
class Backup::Storages
|
8
|
+
class Base
|
9
|
+
def store(id, path)
|
10
|
+
method_not_implemented
|
11
|
+
end
|
12
|
+
|
13
|
+
def delete(id)
|
14
|
+
method_not_implemented
|
15
|
+
end
|
16
|
+
|
17
|
+
def each
|
18
|
+
method_not_implemented
|
19
|
+
end
|
20
|
+
|
21
|
+
class Object
|
22
|
+
attr_reader :storage, :id
|
23
|
+
|
24
|
+
def initialize(storage, id)
|
25
|
+
@storage = storage
|
26
|
+
@id = id
|
27
|
+
end
|
28
|
+
|
29
|
+
def last_modified
|
30
|
+
method_not_implemented
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
id
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# Storage based on some directory in local filesystem
|
6
|
+
#
|
7
|
+
class Backup::Storages
|
8
|
+
class Local < Backup::Storages::Base
|
9
|
+
attr_reader :directory
|
10
|
+
|
11
|
+
def initialize(directory:)
|
12
|
+
@directory = directory.gsub(/\/*\z/, "") # Ensure trailing slash
|
13
|
+
end
|
14
|
+
|
15
|
+
def store(relative_path, file_to_write)
|
16
|
+
FileUtils.mkdir_p(directory)
|
17
|
+
FileUtils.cp_r(file_to_write, File.join(directory, relative_path))
|
18
|
+
Backup::Storages::Local::Object.new(self, relative_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(relative_path)
|
22
|
+
FileUtils.rm_f(File.join(directory, relative_path))
|
23
|
+
end
|
24
|
+
|
25
|
+
def each
|
26
|
+
Dir.glob File.join(directory, "**", "*") do |path|
|
27
|
+
yield Backup::Storages::Local::Object.new(self, path[directory.size+1..-1])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Object < Backup::Storages::Base::Object
|
32
|
+
def last_modified
|
33
|
+
File.mtime(File.join(storage.directory, id)).utc
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Backup::Tasks
|
5
|
+
class Directory
|
6
|
+
def initialize(path, options = {})
|
7
|
+
@path = path
|
8
|
+
# @name = options.fetch(:name)
|
9
|
+
@options = options
|
10
|
+
|
11
|
+
if options[:compressor]
|
12
|
+
@compressor = Symbol === options[:compressor] ? { type: options[:compressor] } : options[:compressor]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def perform(storage)
|
17
|
+
return if !File.readable?(@path) || !File.directory?(@path)
|
18
|
+
|
19
|
+
@options.fetch(:name, @path).tap do |x|
|
20
|
+
@filename = add_extension(construct_filename(File.basename(x, ".*")) + File.extname(x))
|
21
|
+
end
|
22
|
+
|
23
|
+
Tempfile.open do |tempfile|
|
24
|
+
with compression_environment do
|
25
|
+
command "tar", tar_flags, tempfile.path, "-C", @path, "."
|
26
|
+
end
|
27
|
+
storage.store(@filename, tempfile.path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def compression_environment
|
32
|
+
case @compressor&.fetch(:type)
|
33
|
+
when :xz then { XZ_OPT: "-#{@compressor.fetch(:level, 3)}" }
|
34
|
+
when :gzip then { GZIP: "-#{@compressor.fetch(:level, 3)}" }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def tar_flags
|
39
|
+
flags = ["c"]
|
40
|
+
|
41
|
+
flags << "h" if @options.fetch(:symlinks, :follow) == :follow
|
42
|
+
|
43
|
+
case @compressor&.fetch(:type)
|
44
|
+
when :xz then flags << "J"
|
45
|
+
when :gzip then flags << "z"
|
46
|
+
end
|
47
|
+
|
48
|
+
flags << "v"
|
49
|
+
flags << "f"
|
50
|
+
flags.join("")
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_extension(name)
|
54
|
+
case @compressor&.fetch(:type)
|
55
|
+
when :xz then name + ".tar.xz"
|
56
|
+
when :gzip then name + ".tar.gz"
|
57
|
+
else name + ".tar"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Backup::Tasks
|
5
|
+
class MySQL
|
6
|
+
attr_reader :host, :credentials, :options
|
7
|
+
|
8
|
+
def initialize(host:, credentials:, databases: :all, **options)
|
9
|
+
@host = host
|
10
|
+
@credentials = credentials
|
11
|
+
@databases = databases
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform(storage)
|
16
|
+
databases.map do |db|
|
17
|
+
Tempfile.open do |tempfile|
|
18
|
+
command("mysqldump", *credentials.to_options, *host_options, *dump_options, "--databases", db).tap do |dump_sql|
|
19
|
+
stdin dump_sql do
|
20
|
+
command("xz", "--compress", "-9", "--format=xz", "--keep", "--threads=0", "--verbose", "--check=sha256").tap do |dump_xz|
|
21
|
+
tempfile.write(dump_xz)
|
22
|
+
storage.store(construct_filename(db, ".sql.xz"), tempfile.path)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def databases
|
31
|
+
if @databases == :all
|
32
|
+
command("mysql", *credentials.to_options, *host_options, "-e", "SHOW DATABASES;")
|
33
|
+
.split("\n")
|
34
|
+
.reject { |el| el =~ /database|information_schema|mysql|performance_schema|test|phpmyadmin|pma|sys/i }
|
35
|
+
else
|
36
|
+
[@databases].flatten.compact
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def dump_options
|
41
|
+
@options.fetch(:dump_options) do
|
42
|
+
%W( --add-drop-table
|
43
|
+
--add-locks
|
44
|
+
--allow-keywords
|
45
|
+
--comments
|
46
|
+
--complete-insert
|
47
|
+
--create-options
|
48
|
+
--disable-keys
|
49
|
+
--extended-insert
|
50
|
+
--lock-tables
|
51
|
+
--quick
|
52
|
+
--quote-names
|
53
|
+
--routines
|
54
|
+
--set-charset
|
55
|
+
--dump-date
|
56
|
+
--tz-utc
|
57
|
+
--verbose )
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def host_options
|
62
|
+
["--host=#{@host}"]
|
63
|
+
end
|
64
|
+
|
65
|
+
class Credentials
|
66
|
+
def initialize(user:, password:)
|
67
|
+
@user = user
|
68
|
+
@password = password
|
69
|
+
end
|
70
|
+
|
71
|
+
def stringify
|
72
|
+
"--user #{@user} #{stringify_password}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def stringify_password
|
76
|
+
@password.nil? || @password.empty? ? "" : "--password=#{@password}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_options
|
80
|
+
["--user", @user, stringify_password]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/test.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require File.expand_path('../lib/backup-agent', __FILE__)
|
5
|
+
|
6
|
+
credentials do
|
7
|
+
mysql localhost: { user: 'root', password: 'root' }
|
8
|
+
amazon_s3 default: { access_key_id: 'AKIAIJDU5HT7XT6HRINA', secret_access_key: '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' }
|
9
|
+
end
|
10
|
+
|
11
|
+
storages do
|
12
|
+
amazon_s3 default: { region: 'eu-central-1', bucket: 'perseidev-backups' }
|
13
|
+
end
|
14
|
+
|
15
|
+
backup to: { amazon_s3: :default, local: :default } do
|
16
|
+
mysql host: '127.0.0.1', credentials: :localhost, databases: :all
|
17
|
+
Dir['/var/www/*/current/public'].each { |dir| directory dir }
|
18
|
+
end
|
19
|
+
|
20
|
+
delete_backups_older_than 30.days
|
metadata
CHANGED
@@ -1,80 +1,86 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: backup-agent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yaroslav Konoplov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-04-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: '3.0'
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '6.0'
|
34
37
|
type: :runtime
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
37
40
|
requirements:
|
38
41
|
- - ">="
|
39
42
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
43
|
+
version: '3.0'
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '6.0'
|
41
47
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
48
|
+
name: method-not-implemented
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
44
50
|
requirements:
|
45
|
-
- - "
|
51
|
+
- - "~>"
|
46
52
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
53
|
+
version: '1.0'
|
48
54
|
type: :runtime
|
49
55
|
prerelease: false
|
50
56
|
version_requirements: !ruby/object:Gem::Requirement
|
51
57
|
requirements:
|
52
|
-
- - "
|
58
|
+
- - "~>"
|
53
59
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
60
|
+
version: '1.0'
|
55
61
|
description: Easy AWS S3 backup
|
56
62
|
email:
|
57
|
-
-
|
63
|
+
- eahome00@gmail.com
|
58
64
|
executables: []
|
59
65
|
extensions: []
|
60
66
|
extra_rdoc_files: []
|
61
67
|
files:
|
62
68
|
- ".gitignore"
|
63
|
-
-
|
64
|
-
- LICENSE.txt
|
69
|
+
- LICENSE
|
65
70
|
- README.md
|
66
|
-
- Rakefile
|
67
71
|
- backup-agent.gemspec
|
68
72
|
- lib/backup-agent.rb
|
69
|
-
- lib/backup-agent/
|
70
|
-
- lib/backup-agent/
|
71
|
-
- lib/backup-agent/abstract_storage_object.rb
|
72
|
-
- lib/backup-agent/features.rb
|
73
|
+
- lib/backup-agent/credentials.rb
|
74
|
+
- lib/backup-agent/dsl.rb
|
73
75
|
- lib/backup-agent/performer.rb
|
74
|
-
- lib/backup-agent/
|
75
|
-
- lib/backup-agent/
|
76
|
-
- lib/backup-agent/
|
77
|
-
- lib/backup-agent/
|
76
|
+
- lib/backup-agent/storages.rb
|
77
|
+
- lib/backup-agent/storages/amazon-s3.rb
|
78
|
+
- lib/backup-agent/storages/base.rb
|
79
|
+
- lib/backup-agent/storages/local.rb
|
80
|
+
- lib/backup-agent/tasks/directory.rb
|
81
|
+
- lib/backup-agent/tasks/mysql.rb
|
82
|
+
- lib/backup-agent/version.rb
|
83
|
+
- test.rb
|
78
84
|
homepage: http://github.com/yivo/backup-agent
|
79
85
|
licenses:
|
80
86
|
- MIT
|
@@ -95,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
101
|
version: '0'
|
96
102
|
requirements: []
|
97
103
|
rubyforge_project:
|
98
|
-
rubygems_version: 2.
|
104
|
+
rubygems_version: 2.6.8
|
99
105
|
signing_key:
|
100
106
|
specification_version: 4
|
101
107
|
summary: Easy AWS S3 backup
|
data/Gemfile
DELETED
data/Rakefile
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
require 'bundler/gem_tasks'
|
@@ -1,32 +0,0 @@
|
|
1
|
-
module Backup
|
2
|
-
class AbstractStorage
|
3
|
-
include Enumerable
|
4
|
-
|
5
|
-
attr_reader :config, :env
|
6
|
-
|
7
|
-
def initialize(config, env = {})
|
8
|
-
@config = config
|
9
|
-
@env = env
|
10
|
-
end
|
11
|
-
|
12
|
-
def open
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def close
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
def upload(key, path)
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def delete(key)
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
def each
|
29
|
-
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module Backup
|
2
|
-
class AbstractStorageObject
|
3
|
-
attr_reader :storage, :key, :env
|
4
|
-
|
5
|
-
def initialize(storage, key, env = {})
|
6
|
-
@storage = storage
|
7
|
-
@key = key
|
8
|
-
@env = env
|
9
|
-
end
|
10
|
-
|
11
|
-
def last_modified
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
def delete
|
16
|
-
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
module Backup
|
2
|
-
class Features
|
3
|
-
def initialize
|
4
|
-
check_mysql
|
5
|
-
check_mongodb
|
6
|
-
end
|
7
|
-
|
8
|
-
def check_mysql
|
9
|
-
if @mysql_check.nil?
|
10
|
-
@mysql_check = system('/usr/bin/env mysql --version') ? true : (puts('MySQL is not installed'); false)
|
11
|
-
end
|
12
|
-
@mysql_check
|
13
|
-
end
|
14
|
-
|
15
|
-
def check_mongodb
|
16
|
-
if @mongodb_check.nil?
|
17
|
-
@mongodb_check = system('/usr/bin/env mongod --version') ? true : (puts('MongoDB is not installed'))
|
18
|
-
end
|
19
|
-
@mongodb_check
|
20
|
-
end
|
21
|
-
|
22
|
-
def mysql_installed?
|
23
|
-
!!@mysql_check
|
24
|
-
end
|
25
|
-
|
26
|
-
def mongodb_installed?
|
27
|
-
!!@mongodb_check
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module Backup
|
2
|
-
class S3Object < AbstractStorageObject
|
3
|
-
def initialize(*)
|
4
|
-
super
|
5
|
-
@object = env.fetch(:object)
|
6
|
-
@bucket = env.fetch(:bucket)
|
7
|
-
end
|
8
|
-
|
9
|
-
def last_modified
|
10
|
-
@object.last_modified
|
11
|
-
end
|
12
|
-
|
13
|
-
def delete
|
14
|
-
storage.delete(key)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
module Backup
|
2
|
-
class S3Storage < AbstractStorage
|
3
|
-
|
4
|
-
def initialize(*)
|
5
|
-
super
|
6
|
-
@bucket_name = env.fetch(:bucket)
|
7
|
-
end
|
8
|
-
|
9
|
-
def s3
|
10
|
-
@s3 ||= begin
|
11
|
-
Aws.config.update(
|
12
|
-
region: config.region,
|
13
|
-
credentials: Aws::Credentials.new(config.access_key_id, config.secret_access_key)
|
14
|
-
)
|
15
|
-
Aws::S3::Resource.new
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def bucket
|
20
|
-
s3.bucket(@bucket_name)
|
21
|
-
end
|
22
|
-
|
23
|
-
def open
|
24
|
-
s3
|
25
|
-
end
|
26
|
-
|
27
|
-
def upload(key, path)
|
28
|
-
bucket.object(key).upload_file(path)
|
29
|
-
end
|
30
|
-
|
31
|
-
def delete(key)
|
32
|
-
bucket.object(key).delete
|
33
|
-
end
|
34
|
-
|
35
|
-
def object(key)
|
36
|
-
S3Object.new(self, key, object: bucket.object(key), bucket: bucket)
|
37
|
-
end
|
38
|
-
|
39
|
-
def each
|
40
|
-
bucket.objects.each do |s3_obj|
|
41
|
-
yield S3Object.new(self, s3_obj.key, object: s3_obj, bucket: bucket)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
data/lib/backup-agent/task.rb
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
module Backup
|
2
|
-
class Task < Confo::Config
|
3
|
-
def initialize(*)
|
4
|
-
set :mysql_user, 'root'
|
5
|
-
set :mysql_password, 'root'
|
6
|
-
set :mysql_host, 'localhost'
|
7
|
-
set :mysql_databases, -> do
|
8
|
-
`/usr/bin/env mysql #{get(:mysql_connect)} -e "SHOW DATABASES;"`
|
9
|
-
.split("\n")
|
10
|
-
.reject { |el| el =~ /Database|information_schema|mysql|performance_schema|test|phpmyadmin/ }
|
11
|
-
end
|
12
|
-
|
13
|
-
set :mysqldump_options, %w(
|
14
|
-
--add-drop-database
|
15
|
-
--add-drop-table
|
16
|
-
--add-locks
|
17
|
-
--allow-keywords
|
18
|
-
--comments
|
19
|
-
--complete-insert
|
20
|
-
--create-options
|
21
|
-
--debug-check
|
22
|
-
--debug-info
|
23
|
-
--extended-insert
|
24
|
-
--flush-privileges
|
25
|
-
--insert-ignore
|
26
|
-
--lock-tables
|
27
|
-
--quick
|
28
|
-
--quote-names
|
29
|
-
--set-charset
|
30
|
-
--dump-date
|
31
|
-
--secure-auth
|
32
|
-
--tz-utc
|
33
|
-
--disable-keys )
|
34
|
-
|
35
|
-
set :mysql_connect, -> do
|
36
|
-
pass = get(:mysql_password)
|
37
|
-
pass_param = pass && !pass.empty? ? "--password=#{pass}" : ''
|
38
|
-
"--user #{get(:mysql_user)} #{pass_param} --host=#{get(:mysql_host)}"
|
39
|
-
end
|
40
|
-
|
41
|
-
set :mongo_databases, -> do
|
42
|
-
if `/usr/bin/env mongo --eval "db.getMongo().getDBNames()"` =~ /connecting to: (.*)/m
|
43
|
-
$1.split(/[\n,]/).reject(&:empty?)
|
44
|
-
else
|
45
|
-
[]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
set :mongo_host, 'localhost'
|
50
|
-
set :mongo_connect, -> { "-h #{get(:mongo_host)}" }
|
51
|
-
|
52
|
-
set :directories, -> {
|
53
|
-
Dir['/var/www/*'].each_with_object({}) do |el, memo|
|
54
|
-
if Dir.exists?(File.join(el, 'current/public/uploads'))
|
55
|
-
memo["Uploads #{File.basename(el)}"] = File.join(el, 'current/public/uploads')
|
56
|
-
end
|
57
|
-
end
|
58
|
-
}
|
59
|
-
|
60
|
-
set :files, {}
|
61
|
-
|
62
|
-
set :days_to_keep_backups, 30
|
63
|
-
super
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|