backup-agent 1.0.9 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|