astrails-safe 0.1.0 → 0.1.1
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/README +13 -0
- data/lib/astrails/safe.rb +16 -3
- data/lib/astrails/safe/archive.rb +2 -2
- data/lib/astrails/safe/config/builder.rb +2 -2
- data/lib/astrails/safe/config/node.rb +9 -0
- data/lib/astrails/safe/gpg.rb +42 -0
- data/lib/astrails/safe/gzip.rb +25 -0
- data/lib/astrails/safe/local.rb +54 -0
- data/lib/astrails/safe/mysqldump.rb +4 -8
- data/lib/astrails/safe/pipe.rb +15 -0
- data/lib/astrails/safe/s3.rb +68 -0
- data/lib/astrails/safe/sink.rb +33 -0
- data/lib/astrails/safe/source.rb +31 -0
- data/lib/astrails/safe/stream.rb +23 -43
- data/safe.gemspec +10 -3
- data/templates/script.rb +8 -5
- metadata +10 -3
- data/lib/astrails/safe/engine.rb +0 -134
data/README
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Simple mysql and filesystem backups with S3 support
|
2
|
+
|
3
|
+
Usage:
|
4
|
+
astrails-safe [OPTIONS] CONFIG_FILE
|
5
|
+
Options:
|
6
|
+
-h, --help This help screen
|
7
|
+
-v, --verbose be verbose, duh!
|
8
|
+
-n, --dry-run just pretend, don't do anything.
|
9
|
+
-L, --local skip S3
|
10
|
+
|
11
|
+
Note: CONFIG_FILE will be created from template if missing
|
12
|
+
|
13
|
+
See template for configuration examples
|
data/lib/astrails/safe.rb
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
require 'extensions/mktmpdir'
|
2
2
|
require 'astrails/safe/tmp_file'
|
3
|
+
|
3
4
|
require 'astrails/safe/config/node'
|
4
5
|
require 'astrails/safe/config/builder'
|
6
|
+
|
5
7
|
require 'astrails/safe/stream'
|
6
|
-
|
8
|
+
|
9
|
+
require 'astrails/safe/source'
|
7
10
|
require 'astrails/safe/mysqldump'
|
8
11
|
require 'astrails/safe/archive'
|
9
12
|
|
13
|
+
require 'astrails/safe/pipe'
|
14
|
+
require 'astrails/safe/gpg'
|
15
|
+
require 'astrails/safe/gzip'
|
16
|
+
|
17
|
+
require 'astrails/safe/sink'
|
18
|
+
require 'astrails/safe/local'
|
19
|
+
require 'astrails/safe/s3'
|
20
|
+
|
21
|
+
|
10
22
|
module Astrails
|
11
23
|
module Safe
|
12
24
|
ROOT = File.join(File.dirname(__FILE__), "..", "..")
|
@@ -19,8 +31,9 @@ module Astrails
|
|
19
31
|
config = Config::Node.new(&block)
|
20
32
|
#config.dump
|
21
33
|
|
22
|
-
Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases]
|
23
|
-
Astrails::Safe::Archive.run(config[:tar, :archives]
|
34
|
+
Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases])
|
35
|
+
Astrails::Safe::Archive.run(config[:tar, :archives])
|
36
|
+
|
24
37
|
Astrails::Safe::TmpFile.cleanup
|
25
38
|
end
|
26
39
|
end
|
@@ -3,8 +3,8 @@ module Astrails
|
|
3
3
|
module Config
|
4
4
|
class Builder
|
5
5
|
COLLECTIONS = %w/database archive/
|
6
|
-
ITEMS = %w/s3 key secret bucket path
|
7
|
-
user socket tar files exclude/
|
6
|
+
ITEMS = %w/s3 key secret bucket path gpg password keep local mysqldump options
|
7
|
+
user socket tar files exclude filename/
|
8
8
|
NAMES = COLLECTIONS + ITEMS
|
9
9
|
def initialize(node)
|
10
10
|
@node = node
|
@@ -3,6 +3,7 @@ module Astrails
|
|
3
3
|
module Safe
|
4
4
|
module Config
|
5
5
|
class Node
|
6
|
+
attr_reader :parent
|
6
7
|
def initialize(parent = nil, data = {}, &block)
|
7
8
|
@parent, @data = parent, {}
|
8
9
|
data.each { |k, v| self[k] = v }
|
@@ -41,6 +42,14 @@ module Astrails
|
|
41
42
|
end
|
42
43
|
include Enumerable
|
43
44
|
|
45
|
+
def to_hash
|
46
|
+
@data.keys.inject({}) do |res, key|
|
47
|
+
value = @data[key]
|
48
|
+
res[key] = value.is_a?(Node) ? value.to_hash : value
|
49
|
+
res
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
44
53
|
def dump(indent = "")
|
45
54
|
@data.each do |key, value|
|
46
55
|
if value.is_a?(Node)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gpg < Pipe
|
4
|
+
|
5
|
+
def compressed?
|
6
|
+
active? || @parent.compressed?
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
if key
|
13
|
+
rise RuntimeError, "can't use both gpg password and pubkey" if password
|
14
|
+
"|gpg -e -r #{key}"
|
15
|
+
elsif password
|
16
|
+
"|gpg -c --passphrase-file #{gpg_password_file(password)}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def extension
|
21
|
+
".gpg" if active?
|
22
|
+
end
|
23
|
+
|
24
|
+
def active?
|
25
|
+
password || key
|
26
|
+
end
|
27
|
+
|
28
|
+
def password
|
29
|
+
@password ||= config[:gpg, :password]
|
30
|
+
end
|
31
|
+
|
32
|
+
def key
|
33
|
+
@key ||= config[:gpg, :key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def gpg_password_file(pass)
|
37
|
+
return "TEMP_GENERATED_FILENAME" if $DRY_RUN
|
38
|
+
Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Gzip < Pipe
|
4
|
+
|
5
|
+
def compressed?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def pipe
|
12
|
+
"|gzip" if active?
|
13
|
+
end
|
14
|
+
|
15
|
+
def extension
|
16
|
+
".gz" if active?
|
17
|
+
end
|
18
|
+
|
19
|
+
def active?
|
20
|
+
!@parent.compressed?
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Local < Sink
|
4
|
+
|
5
|
+
def open(&block)
|
6
|
+
return @parent.open(&block) unless active?
|
7
|
+
run
|
8
|
+
File.open(path, &block) unless $DRY_RUN
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def active?
|
14
|
+
# S3 can't upload from pipe. it needs to know file size, so we must pass through :local
|
15
|
+
# will change once we add SSH sink
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def prefix
|
20
|
+
@prefix ||= File.expand_path(expand(@config[:local, :path] || raise(RuntimeError, "missing :local/:path in configuration")))
|
21
|
+
end
|
22
|
+
|
23
|
+
def command
|
24
|
+
"#{@parent.command} > #{path}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
puts "command: #{command}" if $_VERBOSE
|
29
|
+
unless $DRY_RUN
|
30
|
+
FileUtils.mkdir_p(prefix) unless File.directory?(prefix)
|
31
|
+
system command
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def cleanup
|
36
|
+
return unless keep = @config[:keep, :local]
|
37
|
+
|
38
|
+
base = File.basename(filename).split(".").first
|
39
|
+
|
40
|
+
pattern = File.join(prefix, "#{base}*")
|
41
|
+
puts "listing files #{pattern.inspect}" if $_VERBOSE
|
42
|
+
files = Dir[pattern] .
|
43
|
+
select{|f| File.file?(f)} .
|
44
|
+
sort
|
45
|
+
|
46
|
+
cleanup_with_limit(files, keep) do |f|
|
47
|
+
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
48
|
+
File.unlink(f) unless $DRY_RUN
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
module Astrails
|
2
2
|
module Safe
|
3
|
-
class Mysqldump <
|
3
|
+
class Mysqldump < Source
|
4
4
|
|
5
5
|
def command
|
6
|
-
"mysqldump
|
6
|
+
@commanbd ||= "mysqldump --defaults-extra-file=#{mysql_password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
|
7
7
|
end
|
8
8
|
|
9
|
-
def extension; 'sql'; end
|
9
|
+
def extension; '.sql'; end
|
10
10
|
|
11
11
|
protected
|
12
12
|
|
13
|
-
def
|
13
|
+
def mysql_password_file
|
14
14
|
Astrails::Safe::TmpFile.create("mysqldump") do |file|
|
15
15
|
file.puts "[mysqldump]"
|
16
16
|
%w/user password socket host port/.each do |k|
|
@@ -26,10 +26,6 @@ module Astrails
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
def mysqldump_extra_options
|
30
|
-
@config[:options] + " " if @config[:options]
|
31
|
-
end
|
32
|
-
|
33
29
|
end
|
34
30
|
end
|
35
31
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class S3 < Sink
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def active?
|
8
|
+
bucket && key && secret
|
9
|
+
end
|
10
|
+
|
11
|
+
def prefix
|
12
|
+
@prefix ||= expand(config[:s3, :path] || expand(config[:local, :path] || ":kind/:id"))
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
# needed in cleanup even on dry run
|
17
|
+
AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true) unless $LOCAL
|
18
|
+
|
19
|
+
file = @parent.open
|
20
|
+
puts "Uploading #{bucket}:#{path}" if $_VERBOSE || $DRY_RUN
|
21
|
+
unless $DRY_RUN || $LOCAL
|
22
|
+
AWS::S3::Bucket.create(bucket)
|
23
|
+
AWS::S3::S3Object.store(path, file, bucket)
|
24
|
+
puts "...done" if $_VERBOSE
|
25
|
+
end
|
26
|
+
file.close if file
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def cleanup
|
31
|
+
|
32
|
+
return if $LOCAL
|
33
|
+
|
34
|
+
return unless keep = @config[:keep, :s3]
|
35
|
+
|
36
|
+
bucket = @config[:s3, :bucket]
|
37
|
+
|
38
|
+
base = File.basename(filename).split(".").first
|
39
|
+
|
40
|
+
puts "listing files in #{bucket}:#{prefix}/#{base}"
|
41
|
+
files = AWS::S3::Bucket.objects(bucket, :prefix => "#{prefix}/#{base}", :max_keys => keep * 2)
|
42
|
+
puts files.collect {|x| x.key} if $_VERBOSE
|
43
|
+
|
44
|
+
files = files.
|
45
|
+
collect {|x| x.key}.
|
46
|
+
sort
|
47
|
+
|
48
|
+
cleanup_with_limit(files, keep) do |f|
|
49
|
+
puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
|
50
|
+
AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN || $LOCAL
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def bucket
|
55
|
+
config[:s3, :bucket]
|
56
|
+
end
|
57
|
+
|
58
|
+
def key
|
59
|
+
config[:s3, :key]
|
60
|
+
end
|
61
|
+
|
62
|
+
def secret
|
63
|
+
config[:s3, :secret]
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Sink < Stream
|
4
|
+
|
5
|
+
def run
|
6
|
+
if active?
|
7
|
+
save
|
8
|
+
cleanup
|
9
|
+
else
|
10
|
+
@parent.run
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
# prefix is defined in subclass
|
17
|
+
def path
|
18
|
+
@path ||= File.join(prefix, filename)
|
19
|
+
end
|
20
|
+
|
21
|
+
# call block on files to be removed (all except for the LAST 'limit' files
|
22
|
+
def cleanup_with_limit(files, limit, &block)
|
23
|
+
return unless files.size > limit
|
24
|
+
|
25
|
+
to_remove = files[0..(files.size - limit - 1)]
|
26
|
+
# TODO: validate here
|
27
|
+
to_remove.each(&block)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Source < Stream
|
4
|
+
|
5
|
+
def initialize(id, config)
|
6
|
+
@id, @config = id, config
|
7
|
+
end
|
8
|
+
|
9
|
+
def filename
|
10
|
+
@filename ||= expand(":kind-:id.:timestamp#{extension}")
|
11
|
+
end
|
12
|
+
|
13
|
+
# process each config key as source (with full pipe)
|
14
|
+
def self.run(config)
|
15
|
+
unless config
|
16
|
+
puts "No configuration found for #{kind}"
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
config.each do |key, value|
|
21
|
+
stream = [Gpg, Gzip, Local, S3].inject(new(key, value)) do |res, klass|
|
22
|
+
klass.new(res)
|
23
|
+
end
|
24
|
+
stream.run
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
data/lib/astrails/safe/stream.rb
CHANGED
@@ -2,64 +2,44 @@ module Astrails
|
|
2
2
|
module Safe
|
3
3
|
class Stream
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
@config, @command, @filename = config, command, filename
|
5
|
+
def initialize(parent)
|
6
|
+
@parent = parent
|
8
7
|
end
|
9
8
|
|
10
|
-
def
|
11
|
-
|
12
|
-
redirect
|
13
|
-
execute
|
9
|
+
def id
|
10
|
+
@id ||= @parent.id
|
14
11
|
end
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
def gpg_password_file(pass)
|
19
|
-
Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
|
13
|
+
def config
|
14
|
+
@config ||= @parent.config
|
20
15
|
end
|
21
16
|
|
22
|
-
def
|
23
|
-
|
24
|
-
password = @config[:gpg, :password]
|
25
|
-
key = @config[:gpg, :key]
|
26
|
-
|
27
|
-
return false unless key || password
|
28
|
-
|
29
|
-
if key
|
30
|
-
rise RuntimeError, "can't use both gpg password and pubkey" if password
|
31
|
-
|
32
|
-
@filename << ".gpg"
|
33
|
-
@command << "|gpg -e -r #{key}"
|
34
|
-
else
|
35
|
-
@filename << ".gpg"
|
36
|
-
unless $DRY_RUN
|
37
|
-
@command << "|gpg -c --passphrase-file #{gpg_password_file(password)}"
|
38
|
-
else
|
39
|
-
@command << "|gpg -c --passphrase-file TEMP_GENERATED_FILENAME"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
true
|
17
|
+
def filename
|
18
|
+
@parent.filename
|
43
19
|
end
|
44
20
|
|
45
|
-
def
|
46
|
-
@
|
47
|
-
@command << "|gzip"
|
21
|
+
def compressed?
|
22
|
+
@parent && @parent.compressed?
|
48
23
|
end
|
49
24
|
|
50
|
-
|
51
|
-
|
25
|
+
protected
|
26
|
+
|
27
|
+
def name
|
28
|
+
self.class.name.split('::').last.downcase
|
52
29
|
end
|
53
30
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
31
|
+
def kind
|
32
|
+
@parent ? @parent.kind : name
|
33
|
+
end
|
57
34
|
|
58
|
-
|
59
|
-
|
60
|
-
|
35
|
+
def expand(path)
|
36
|
+
path .
|
37
|
+
gsub(/:kind\b/, kind) .
|
38
|
+
gsub(/:id\b/, id) .
|
39
|
+
gsub(/:timestamp\b/, timestamp)
|
61
40
|
end
|
62
41
|
|
63
42
|
end
|
64
43
|
end
|
65
44
|
end
|
45
|
+
|
data/safe.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "safe"
|
3
|
-
s.version = "0.1.
|
3
|
+
s.version = "0.1.1"
|
4
4
|
s.date = "2009-03-15"
|
5
5
|
s.summary = "Astrails Safe"
|
6
6
|
s.email = "we@astrails.com"
|
@@ -9,16 +9,23 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.has_rdoc = false
|
10
10
|
s.authors = ["Astrails Ltd."]
|
11
11
|
s.files = files = %w(
|
12
|
+
README
|
12
13
|
bin/astrails-safe
|
14
|
+
lib/extensions/mktmpdir.rb
|
13
15
|
lib/astrails/safe.rb
|
16
|
+
lib/astrails/safe/s3.rb
|
17
|
+
lib/astrails/safe/gpg.rb
|
14
18
|
lib/astrails/safe/mysqldump.rb
|
15
19
|
lib/astrails/safe/stream.rb
|
16
20
|
lib/astrails/safe/config/builder.rb
|
17
21
|
lib/astrails/safe/config/node.rb
|
18
|
-
lib/astrails/safe/
|
22
|
+
lib/astrails/safe/sink.rb
|
23
|
+
lib/astrails/safe/pipe.rb
|
24
|
+
lib/astrails/safe/source.rb
|
19
25
|
lib/astrails/safe/archive.rb
|
26
|
+
lib/astrails/safe/local.rb
|
20
27
|
lib/astrails/safe/tmp_file.rb
|
21
|
-
lib/
|
28
|
+
lib/astrails/safe/gzip.rb
|
22
29
|
templates/script.rb
|
23
30
|
safe.gemspec
|
24
31
|
)
|
data/templates/script.rb
CHANGED
@@ -1,21 +1,24 @@
|
|
1
1
|
safe do
|
2
|
-
|
3
|
-
#
|
2
|
+
|
3
|
+
# backup file path (not including filename)
|
4
4
|
# supported substitutions:
|
5
5
|
# :kind -> backup 'engine' kind, e.g. "mysqldump" or "archive"
|
6
6
|
# :id -> backup 'id', e.g. "blog", "production", etc.
|
7
7
|
# :timestamp -> current run timestamp (same for all the backups in the same 'run')
|
8
8
|
# you can set separate :path for all backups (or once globally here)
|
9
|
-
|
9
|
+
local do
|
10
|
+
path "/backup/:kind/"
|
11
|
+
end
|
10
12
|
|
11
13
|
## uncomment to enable uploads to Amazon S3
|
12
14
|
## Amazon S3 auth (optional)
|
15
|
+
## don't forget to add :s3 to the 'store' list
|
13
16
|
# s3 do
|
14
17
|
# key YOUR_S3_KEY
|
15
18
|
# secret YOUR_S3_SECRET
|
16
19
|
# bucket S3_BUCKET
|
17
|
-
# #
|
18
|
-
#
|
20
|
+
# # path for uploads to S3. supports same substitution like :local/:path
|
21
|
+
# path ":kind/" # this is default
|
19
22
|
# end
|
20
23
|
|
21
24
|
## alternative style:
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: astrails-safe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Astrails Ltd.
|
@@ -31,16 +31,23 @@ extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
32
32
|
|
33
33
|
files:
|
34
|
+
- README
|
34
35
|
- bin/astrails-safe
|
36
|
+
- lib/extensions/mktmpdir.rb
|
35
37
|
- lib/astrails/safe.rb
|
38
|
+
- lib/astrails/safe/s3.rb
|
39
|
+
- lib/astrails/safe/gpg.rb
|
36
40
|
- lib/astrails/safe/mysqldump.rb
|
37
41
|
- lib/astrails/safe/stream.rb
|
38
42
|
- lib/astrails/safe/config/builder.rb
|
39
43
|
- lib/astrails/safe/config/node.rb
|
40
|
-
- lib/astrails/safe/
|
44
|
+
- lib/astrails/safe/sink.rb
|
45
|
+
- lib/astrails/safe/pipe.rb
|
46
|
+
- lib/astrails/safe/source.rb
|
41
47
|
- lib/astrails/safe/archive.rb
|
48
|
+
- lib/astrails/safe/local.rb
|
42
49
|
- lib/astrails/safe/tmp_file.rb
|
43
|
-
- lib/
|
50
|
+
- lib/astrails/safe/gzip.rb
|
44
51
|
- templates/script.rb
|
45
52
|
- safe.gemspec
|
46
53
|
has_rdoc: false
|
data/lib/astrails/safe/engine.rb
DELETED
@@ -1,134 +0,0 @@
|
|
1
|
-
module Astrails
|
2
|
-
module Safe
|
3
|
-
class Engine
|
4
|
-
|
5
|
-
def self.run(config, timestamp)
|
6
|
-
unless config
|
7
|
-
puts "No configuration found for #{kind}"
|
8
|
-
return
|
9
|
-
end
|
10
|
-
config.each do |key, value|
|
11
|
-
new(key, value, timestamp).run
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
attr_accessor :timestamp
|
16
|
-
attr_reader :id, :config
|
17
|
-
def initialize(id, config, timestamp)
|
18
|
-
@config, @id, @timestamp = config, id, timestamp
|
19
|
-
end
|
20
|
-
|
21
|
-
def run
|
22
|
-
puts "#{kind}: #{@id}" if $_VERBOSE
|
23
|
-
|
24
|
-
stream = Stream.new(@config, command, backup_filepath)
|
25
|
-
stream.run # execute backup comand. result is file stream.filename
|
26
|
-
|
27
|
-
# UPLOAD
|
28
|
-
upload(stream.filename)
|
29
|
-
|
30
|
-
# CLEANUP
|
31
|
-
cleanup_s3(stream.filename)
|
32
|
-
cleanup_local(stream.filename)
|
33
|
-
end
|
34
|
-
|
35
|
-
protected
|
36
|
-
|
37
|
-
def self.kind
|
38
|
-
name.split('::').last.downcase
|
39
|
-
end
|
40
|
-
def kind
|
41
|
-
self.class.kind
|
42
|
-
end
|
43
|
-
|
44
|
-
def expand(path)
|
45
|
-
path .
|
46
|
-
gsub(/:kind\b/, kind) .
|
47
|
-
gsub(/:id\b/, id) .
|
48
|
-
gsub(/:timestamp\b/, timestamp)
|
49
|
-
end
|
50
|
-
|
51
|
-
def s3_prefix
|
52
|
-
@s3_prefix ||= expand(@config[:s3, :prefix] || ":kind/:id")
|
53
|
-
end
|
54
|
-
|
55
|
-
def backup_filepath
|
56
|
-
@backup_filepath ||= File.expand_path(expand(@config[:path] || raise(RuntimeError, "missing :path in configuration"))) << "." << extension
|
57
|
-
end
|
58
|
-
|
59
|
-
|
60
|
-
def upload(filename)
|
61
|
-
|
62
|
-
bucket = @config[:s3, :bucket]
|
63
|
-
key = @config[:s3, :key]
|
64
|
-
secret = @config[:s3, :secret]
|
65
|
-
|
66
|
-
return unless bucket && key && secret
|
67
|
-
|
68
|
-
upload_path = File.join(s3_prefix, File.basename(filename))
|
69
|
-
|
70
|
-
puts "Uploading file #{filename} to #{bucket}/#{upload_path}" if $_VERBOSE || $DRY_RUN
|
71
|
-
if $LOCAL
|
72
|
-
puts "skip upload (local operation)"
|
73
|
-
else
|
74
|
-
# needed in cleanup even on dry run
|
75
|
-
AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true)
|
76
|
-
|
77
|
-
unless $DRY_RUN
|
78
|
-
AWS::S3::Bucket.create(bucket)
|
79
|
-
AWS::S3::S3Object.store(upload_path, open(filename), bucket)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
puts "...done" if $_VERBOSE
|
83
|
-
end
|
84
|
-
|
85
|
-
# call block on files to be removed (all except for the LAST 'limit' files
|
86
|
-
def cleanup_files(files, limit, &block)
|
87
|
-
return unless files.size > limit
|
88
|
-
|
89
|
-
to_remove = files[0..(files.size - limit - 1)]
|
90
|
-
to_remove.each(&block)
|
91
|
-
end
|
92
|
-
|
93
|
-
def cleanup_local(filename)
|
94
|
-
return unless keep = @config[:keep, :local]
|
95
|
-
|
96
|
-
dir = File.dirname(filename)
|
97
|
-
base = File.basename(filename).split(".").first
|
98
|
-
|
99
|
-
files = Dir[File.join(dir, "#{base}*")] .
|
100
|
-
select{|f| File.file?(f)} .
|
101
|
-
sort
|
102
|
-
|
103
|
-
cleanup_files(files, keep) do |f|
|
104
|
-
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
105
|
-
File.unlink(f) unless $DRY_RUN
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def cleanup_s3(filename)
|
110
|
-
|
111
|
-
return unless keep = @config[:keep, :s3]
|
112
|
-
|
113
|
-
bucket = @config[:s3, :bucket]
|
114
|
-
|
115
|
-
base = File.basename(filename).split(".").first
|
116
|
-
|
117
|
-
puts "listing files in #{bucket}:#{s3_prefix}"
|
118
|
-
files = AWS::S3::Bucket.objects(bucket, :prefix => "#{s3_prefix}/#{base}", :max_keys => keep * 2)
|
119
|
-
puts files.collect {|x| x.key} if $_VERBOSE
|
120
|
-
|
121
|
-
files = files.
|
122
|
-
collect {|x| x.key}.
|
123
|
-
sort
|
124
|
-
|
125
|
-
cleanup_files(files, keep) do |f|
|
126
|
-
puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
|
127
|
-
AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|