astrails-safe 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/astrails/safe.rb +26 -0
- data/lib/astrails/safe/archive.rb +22 -0
- data/lib/astrails/safe/config/builder.rb +58 -0
- data/lib/astrails/safe/config/node.rb +58 -0
- data/lib/astrails/safe/engine.rb +133 -0
- data/lib/astrails/safe/mysqldump.rb +33 -0
- data/lib/astrails/safe/stream.rb +65 -0
- data/lib/astrails/safe/tmp_file.rb +21 -0
- data/safe.gemspec +13 -4
- data/templates/script.rb +106 -0
- metadata +10 -1
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'astrails/safe/tmp_file'
|
2
|
+
require 'astrails/safe/config/node'
|
3
|
+
require 'astrails/safe/config/builder'
|
4
|
+
require 'astrails/safe/stream'
|
5
|
+
require 'astrails/safe/engine'
|
6
|
+
require 'astrails/safe/mysqldump'
|
7
|
+
require 'astrails/safe/archive'
|
8
|
+
|
9
|
+
module Astrails
|
10
|
+
module Safe
|
11
|
+
ROOT = File.join(File.dirname(__FILE__), "..", "..")
|
12
|
+
|
13
|
+
def timestamp
|
14
|
+
@timestamp ||= Time.now.strftime("%y%m%d-%H%M")
|
15
|
+
end
|
16
|
+
|
17
|
+
def safe(&block)
|
18
|
+
config = Config::Node.new(&block)
|
19
|
+
#config.dump
|
20
|
+
|
21
|
+
Astrails::Safe::Mysqldump.run(config[:mysqldump, :databases], timestamp)
|
22
|
+
Astrails::Safe::Archive.run(config[:tar, :archives], timestamp)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Archive < Engine
|
4
|
+
|
5
|
+
def command
|
6
|
+
"tar -cf - #{@config[:options]} #{tar_exclude_files} #{tar_files}"
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def tar_exclude_files
|
12
|
+
[*@config[:exclude]].compact.map{|x| "--exclude=#{x}"} * " "
|
13
|
+
end
|
14
|
+
|
15
|
+
def tar_files
|
16
|
+
raise RuntimeError, "missing files for tar" unless @config[:files]
|
17
|
+
[*@config[:files]] * " "
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
module Config
|
4
|
+
class Builder
|
5
|
+
COLLECTIONS = %w/database archive/
|
6
|
+
ITEMS = %w/s3 key secret bucket path gpg password keep local mysqldump options
|
7
|
+
user socket tar files exclude/
|
8
|
+
NAMES = COLLECTIONS + ITEMS
|
9
|
+
def initialize(node)
|
10
|
+
@node = node
|
11
|
+
end
|
12
|
+
|
13
|
+
# supported args:
|
14
|
+
# args = [value]
|
15
|
+
# args = [id, data]
|
16
|
+
# args = [data]
|
17
|
+
# id/value - simple values, data - hash
|
18
|
+
def method_missing(sym, *args, &block)
|
19
|
+
return super unless NAMES.include?(sym.to_s)
|
20
|
+
|
21
|
+
# do we have id or value?
|
22
|
+
unless args.first.is_a?(Hash)
|
23
|
+
id_or_value = args.shift # nil for args == []
|
24
|
+
end
|
25
|
+
|
26
|
+
# do we have data hash?
|
27
|
+
if data = args.shift
|
28
|
+
die "#{sym}: hash expected: #{data.inspect}" unless data.is_a?(Hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
#puts "#{sym}: args=#{args.inspect}, id_or_value=#{id_or_value}, data=#{data.inspect}, block=#{block.inspect}"
|
32
|
+
|
33
|
+
die "#{sym}: unexpected: #{args.inspect}" unless args.empty?
|
34
|
+
die "#{sym}: missing arguments" unless id_or_value || data || block
|
35
|
+
|
36
|
+
if COLLECTIONS.include?(sym.to_s) && id_or_value
|
37
|
+
data ||= {}
|
38
|
+
end
|
39
|
+
|
40
|
+
if !data && !block
|
41
|
+
# simple value assignment
|
42
|
+
@node[sym] = id_or_value
|
43
|
+
|
44
|
+
elsif id_or_value
|
45
|
+
# collection element with id => create collection node and a subnode in it
|
46
|
+
key = sym.to_s + "s"
|
47
|
+
collection = @node[key] || @node.set(key, {})
|
48
|
+
collection.set(id_or_value, data || {}, &block)
|
49
|
+
|
50
|
+
else
|
51
|
+
# simple subnode
|
52
|
+
@node.set(sym, data || {}, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'astrails/safe/config/builder'
|
2
|
+
module Astrails
|
3
|
+
module Safe
|
4
|
+
module Config
|
5
|
+
class Node
|
6
|
+
def initialize(parent = nil, data = {}, &block)
|
7
|
+
@parent, @data = parent, {}
|
8
|
+
data.each { |k, v| self[k] = v }
|
9
|
+
Builder.new(self).instance_eval(&block) if block
|
10
|
+
end
|
11
|
+
|
12
|
+
# looks for the path from this node DOWN. will not delegate to parent
|
13
|
+
def get(*path)
|
14
|
+
key = path.shift
|
15
|
+
value = @data[key.to_s]
|
16
|
+
return value if value && path.empty?
|
17
|
+
|
18
|
+
value && value.get(*path)
|
19
|
+
end
|
20
|
+
|
21
|
+
# recursive find
|
22
|
+
# starts at the node and continues to the parent
|
23
|
+
def find(*path)
|
24
|
+
get(*path) || @parent && @parent.find(*path)
|
25
|
+
end
|
26
|
+
alias :[] :find
|
27
|
+
|
28
|
+
def set(key, value, &block)
|
29
|
+
@data[key.to_s] =
|
30
|
+
if value.is_a?(Hash)
|
31
|
+
Node.new(self, value, &block)
|
32
|
+
else
|
33
|
+
raise(ArgumentError, "#{key}: no block supported for simple values") if block
|
34
|
+
value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias :[]= :set
|
38
|
+
|
39
|
+
def each(&block)
|
40
|
+
@data.each(&block)
|
41
|
+
end
|
42
|
+
include Enumerable
|
43
|
+
|
44
|
+
def dump(indent = "")
|
45
|
+
@data.each do |key, value|
|
46
|
+
if value.is_a?(Node)
|
47
|
+
puts "#{indent}#{key}:"
|
48
|
+
value.dump(indent + " ")
|
49
|
+
else
|
50
|
+
puts "#{indent}#{key}: #{value.inspect}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,133 @@
|
|
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, File.join(path, backup_filename))
|
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 kind
|
38
|
+
self.class.name.split('::').last.downcase
|
39
|
+
end
|
40
|
+
|
41
|
+
def backup_filename
|
42
|
+
@backup_filename ||= "#{kind}-#{id}.#{timestamp}.tar"
|
43
|
+
end
|
44
|
+
|
45
|
+
def expand(path)
|
46
|
+
path.gsub(/:kind\b/, kind).gsub(/:id\b/, id)
|
47
|
+
end
|
48
|
+
|
49
|
+
def s3_path
|
50
|
+
@s3_path ||= expand(@config[:s3, :path] || ":kind/:id")
|
51
|
+
end
|
52
|
+
|
53
|
+
def path
|
54
|
+
@path ||= File.expand_path(expand(@config[:path] || raise(RuntimeError, "missing :path in configuration")))
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def upload(filename)
|
59
|
+
|
60
|
+
bucket = @config[:s3, :bucket]
|
61
|
+
key = @config[:s3, :key]
|
62
|
+
secret = @config[:s3, :secret]
|
63
|
+
|
64
|
+
return unless bucket && key && secret
|
65
|
+
|
66
|
+
upload_path = File.join(s3_path, File.basename(filename))
|
67
|
+
|
68
|
+
puts "Uploading file #{filename} to #{bucket}/#{upload_path}" if $_VERBOSE || $DRY_RUN
|
69
|
+
if $LOCAL
|
70
|
+
puts "skip upload (local operation)"
|
71
|
+
else
|
72
|
+
# needed in cleanup even on dry run
|
73
|
+
AWS::S3::Base.establish_connection!(:access_key_id => key, :secret_access_key => secret, :use_ssl => true)
|
74
|
+
|
75
|
+
unless $DRY_RUN
|
76
|
+
AWS::S3::Bucket.create(bucket)
|
77
|
+
AWS::S3::S3Object.store(s3_path, open(filename), bucket)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
puts "...done" if $_VERBOSE
|
81
|
+
end
|
82
|
+
|
83
|
+
# call block on files to be removed (all except for the LAST 'limit' files
|
84
|
+
def cleanup_files(files, limit, &block)
|
85
|
+
return unless files.size > limit
|
86
|
+
|
87
|
+
to_remove = files[0..(files.size - limit - 1)]
|
88
|
+
to_remove.each(&block)
|
89
|
+
end
|
90
|
+
|
91
|
+
def cleanup_local(filename)
|
92
|
+
return unless keep = @config[:keep, :local]
|
93
|
+
|
94
|
+
dir = File.dirname(filename)
|
95
|
+
base = File.basename(filename).split(".").first
|
96
|
+
|
97
|
+
files = Dir[File.join(dir, "#{base}*")] .
|
98
|
+
select{|f| File.file?(f)} .
|
99
|
+
sort
|
100
|
+
|
101
|
+
cleanup_files(files, keep) do |f|
|
102
|
+
puts "removing local file #{f}" if $DRY_RUN || $_VERBOSE
|
103
|
+
File.unlink(f) unless $DRY_RUN
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def cleanup_s3(filename)
|
108
|
+
|
109
|
+
return unless keep = @config[:keep, :s3]
|
110
|
+
|
111
|
+
bucket = @config[:s3, :bucket]
|
112
|
+
|
113
|
+
base = File.basename(filename).split(".").first
|
114
|
+
|
115
|
+
puts "listing files in #{bucket}:#{s3_path}"
|
116
|
+
files = AWS::S3::Bucket.objects(bucket, :prefix => s3_path, :max_keys => keep * 2)
|
117
|
+
puts files.collect(&:key) if $_VERBOSE
|
118
|
+
|
119
|
+
files = files.
|
120
|
+
collect(&:key).
|
121
|
+
select{|f| File.basename(f)[0..(base.length - 1)] == base}.
|
122
|
+
sort
|
123
|
+
|
124
|
+
cleanup_files(files, keep) do |f|
|
125
|
+
puts "removing s3 file #{bucket}:#{f}" if $DRY_RUN || $_VERBOSE
|
126
|
+
AWS::S3::Bucket.find(bucket)[f].delete unless $DRY_RUN
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Mysqldump < Engine
|
4
|
+
|
5
|
+
def command
|
6
|
+
"mysqldump --defaults-extra-file=#{password_file} #{@config[:options]} #{mysql_skip_tables} #{@id}"
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def password_file
|
12
|
+
Astrails::Safe::TmpFile.create("mysqldump") do |file|
|
13
|
+
file.puts "[mysqldump]"
|
14
|
+
%w/user password socket host port/.each do |k|
|
15
|
+
v = @config[k]
|
16
|
+
file.puts "#{k} = #{v}" if v
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def mysql_skip_tables
|
22
|
+
if skip_tables = @config[:skip_tables]
|
23
|
+
[*skip_tables].map { |t| "--ignore-table=#{@id}.#{t}" } * " "
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def mysqldump_extra_options
|
28
|
+
@config[:options] + " " if @config[:options]
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Astrails
|
2
|
+
module Safe
|
3
|
+
class Stream
|
4
|
+
|
5
|
+
attr_accessor :config, :command, :filename
|
6
|
+
def initialize(config, command, filename)
|
7
|
+
@config, @command, @filename = config, command, filename
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
encrypt || compress # use gpg or gzip
|
12
|
+
redirect
|
13
|
+
execute
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def gpg_password_file(pass)
|
19
|
+
Astrails::Safe::TmpFile.create("gpg-pass") { |file| file.write(pass) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def encrypt
|
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
|
43
|
+
end
|
44
|
+
|
45
|
+
def compress
|
46
|
+
@filename << ".gz"
|
47
|
+
@command << "|gzip"
|
48
|
+
end
|
49
|
+
|
50
|
+
def redirect
|
51
|
+
@command << ">" << @filename
|
52
|
+
end
|
53
|
+
|
54
|
+
def execute
|
55
|
+
dir = File.dirname(filename)
|
56
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir) || $DRY_RUN
|
57
|
+
|
58
|
+
# EXECUTE
|
59
|
+
puts "Backup command: #{@command}" if $DRY_RUN || $_VERBOSE
|
60
|
+
system @command unless $DRY_RUN
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
module Astrails
|
3
|
+
module Safe
|
4
|
+
module TmpFile
|
5
|
+
@KEEP_FILES = []
|
6
|
+
TMPDIR = Dir.mktmpdir
|
7
|
+
|
8
|
+
def self.create(name)
|
9
|
+
# create temp directory
|
10
|
+
|
11
|
+
file = Tempfile.new(name, TMPDIR)
|
12
|
+
|
13
|
+
yield file
|
14
|
+
|
15
|
+
file.close
|
16
|
+
@KEEP_FILES << file # so that it will not get gcollected and removed from filesystem until the end
|
17
|
+
file.path
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/safe.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "safe"
|
3
|
-
s.version = "0.0.
|
3
|
+
s.version = "0.0.8"
|
4
4
|
s.date = "2009-03-15"
|
5
5
|
s.summary = "Astrails Safe"
|
6
6
|
s.email = "we@astrails.com"
|
@@ -9,9 +9,18 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.has_rdoc = false
|
10
10
|
s.authors = ["Astrails Ltd."]
|
11
11
|
s.files = files = %w(
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
bin/astrails-safe
|
13
|
+
lib/astrails/safe.rb
|
14
|
+
lib/astrails/safe/mysqldump.rb
|
15
|
+
lib/astrails/safe/stream.rb
|
16
|
+
lib/astrails/safe/config/builder.rb
|
17
|
+
lib/astrails/safe/config/node.rb
|
18
|
+
lib/astrails/safe/engine.rb
|
19
|
+
lib/astrails/safe/archive.rb
|
20
|
+
lib/astrails/safe/tmp_file.rb
|
21
|
+
templates/script.rb
|
22
|
+
safe.gemspec
|
23
|
+
)
|
15
24
|
s.executables = files.grep(/^bin/).map {|x| x.gsub(/^bin\//, "")}
|
16
25
|
|
17
26
|
s.test_files = []
|
data/templates/script.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
safe do
|
2
|
+
# global path
|
3
|
+
# supported substitutions:
|
4
|
+
# :kind -> backup 'engine' kind, e.g. "mysqldump" or "archive"
|
5
|
+
# :id -> backup 'id', e.g. "blog", "production", etc.
|
6
|
+
# you can set separate :path for all backups (or once globally here)
|
7
|
+
path "/backup/:kind/:id"
|
8
|
+
|
9
|
+
## uncomment to enable uploads to Amazon S3
|
10
|
+
## Amazon S3 auth (optional)
|
11
|
+
# s3 do
|
12
|
+
# key YOUR_S3_KEY
|
13
|
+
# secret YOUR_S3_SECRET
|
14
|
+
# bucket S3_BUCKET
|
15
|
+
# end
|
16
|
+
|
17
|
+
## alternative style:
|
18
|
+
# s3 :key => YOUR_S3_KEY, :secret => YOUR_S3_SECRET, :bucket => S3_BUCKET
|
19
|
+
|
20
|
+
## uncomment to enable GPG encryption.
|
21
|
+
## Note: you can use public 'key' or symmetric password but not both!
|
22
|
+
# gpg do
|
23
|
+
# # key "backup@astrails.com"
|
24
|
+
# password "astrails"
|
25
|
+
# end
|
26
|
+
|
27
|
+
## uncomment to enable backup rotation. keep only given number of latest
|
28
|
+
## backups. remove the rest
|
29
|
+
# keep do
|
30
|
+
# local 4 # keep 4 local backups
|
31
|
+
# s3 20 # keep 20 S3 backups
|
32
|
+
# end
|
33
|
+
|
34
|
+
# backup mysql databases with mysqldump
|
35
|
+
mysqldump do
|
36
|
+
# you can override any setting from parent in a child:
|
37
|
+
path "/backup/mysql/:id"
|
38
|
+
|
39
|
+
options "-ceKq --single-transaction --create-options"
|
40
|
+
|
41
|
+
user "astrails"
|
42
|
+
password ""
|
43
|
+
# host "localhost"
|
44
|
+
# port 3306
|
45
|
+
socket "/var/run/mysqld/mysqld.sock"
|
46
|
+
|
47
|
+
# database is a 'collection' element. it must have a hash or block parameter
|
48
|
+
# it will be 'collected' in a 'databases', with database id (1st arg) used as hash key
|
49
|
+
# the following code will create mysqldump/databases/blog and mysqldump/databases/mysql ocnfiguration 'nodes'
|
50
|
+
|
51
|
+
# backup database with default values
|
52
|
+
# database :blog
|
53
|
+
|
54
|
+
# backup overriding some values
|
55
|
+
# database :production do
|
56
|
+
# # you can override 'partially'
|
57
|
+
# keep :local => 3
|
58
|
+
# # keep/local is 3, and keep/s3 is 20 (from parent)
|
59
|
+
|
60
|
+
# # local override for gpg password
|
61
|
+
# gpg do
|
62
|
+
# password "custom-production-pass"
|
63
|
+
# end
|
64
|
+
|
65
|
+
# skip_tables [:logger_exceptions, :request_logs] # skip those tables during backup
|
66
|
+
# end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
tar do
|
72
|
+
path "/backup/archives"
|
73
|
+
|
74
|
+
# 'archive' is a collection item, just like 'database'
|
75
|
+
# archive "git-repositories" do
|
76
|
+
# # files and directories to backup
|
77
|
+
# files "/home/git/repositories"
|
78
|
+
# end
|
79
|
+
|
80
|
+
# archive "etc-files" do
|
81
|
+
# files "/etc"
|
82
|
+
# # exlude those files/directories
|
83
|
+
# exclude "/etc/puppet/other"
|
84
|
+
# end
|
85
|
+
|
86
|
+
# archive "dot-configs" do
|
87
|
+
# files "/home/*/.[^.]*"
|
88
|
+
# end
|
89
|
+
|
90
|
+
# archive "blog" do
|
91
|
+
# files "/var/www/blog.astrails.com/"
|
92
|
+
# # specify multiple files/directories as array
|
93
|
+
# exclude ["/var/www/blog.astrails.com/log", "/var/www/blog.astrails.com/tmp"]
|
94
|
+
# end
|
95
|
+
|
96
|
+
# archive "site" do
|
97
|
+
# files "/var/www/astrails.com/"
|
98
|
+
# exclude ["/var/www/astrails.com/log", "/var/www/astrails.com/tmp"]
|
99
|
+
# end
|
100
|
+
|
101
|
+
# archive :misc do
|
102
|
+
# files [ "/backup/*.rb" ]
|
103
|
+
# end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
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.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Astrails Ltd.
|
@@ -32,6 +32,15 @@ extra_rdoc_files: []
|
|
32
32
|
|
33
33
|
files:
|
34
34
|
- bin/astrails-safe
|
35
|
+
- lib/astrails/safe.rb
|
36
|
+
- lib/astrails/safe/mysqldump.rb
|
37
|
+
- lib/astrails/safe/stream.rb
|
38
|
+
- lib/astrails/safe/config/builder.rb
|
39
|
+
- lib/astrails/safe/config/node.rb
|
40
|
+
- lib/astrails/safe/engine.rb
|
41
|
+
- lib/astrails/safe/archive.rb
|
42
|
+
- lib/astrails/safe/tmp_file.rb
|
43
|
+
- templates/script.rb
|
35
44
|
- safe.gemspec
|
36
45
|
has_rdoc: false
|
37
46
|
homepage: http://github.com/astrails/safe
|