dabcup 0.1.2
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/LICENSE +50 -0
- data/README.rdoc +14 -0
- data/bin/dabcup +18 -0
- data/config/dabcup.yml +16 -0
- data/lib/dabcup.rb +16 -0
- data/lib/dabcup/app.rb +60 -0
- data/lib/dabcup/database.rb +59 -0
- data/lib/dabcup/help.rb +86 -0
- data/lib/dabcup/operation.rb +29 -0
- data/lib/dabcup/operation/base.rb +54 -0
- data/lib/dabcup/operation/clean.rb +19 -0
- data/lib/dabcup/operation/clear.rb +10 -0
- data/lib/dabcup/operation/dump.rb +50 -0
- data/lib/dabcup/operation/get.rb +20 -0
- data/lib/dabcup/operation/list.rb +23 -0
- data/lib/dabcup/operation/populate.rb +20 -0
- data/lib/dabcup/operation/remove.rb +15 -0
- data/lib/dabcup/operation/restore.rb +41 -0
- data/lib/dabcup/storage.rb +99 -0
- data/lib/dabcup/storage/driver/base.rb +66 -0
- data/lib/dabcup/storage/driver/ftp.rb +72 -0
- data/lib/dabcup/storage/driver/local.rb +52 -0
- data/lib/dabcup/storage/driver/s3.rb +71 -0
- data/lib/dabcup/storage/driver/sftp.rb +80 -0
- data/lib/dabcup/storage/dump.rb +34 -0
- metadata +112 -0
data/LICENSE
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
Dabcup is copyrighted free software by Alexis Bernard <alexis [at] obloh [dot] com>.
|
2
|
+
|
3
|
+
1. You may make and give away verbatim copies of the source form of the
|
4
|
+
software without restriction, provided that you duplicate all of the
|
5
|
+
original copyright notices and associated disclaimers.
|
6
|
+
|
7
|
+
2. You may modify your copy of the software in any way, provided that
|
8
|
+
you do at least ONE of the following:
|
9
|
+
|
10
|
+
a) place your modifications in the Public Domain or otherwise
|
11
|
+
make them Freely Available, such as by posting said
|
12
|
+
modifications to Usenet or an equivalent medium, or by allowing
|
13
|
+
the author to include your modifications in the software.
|
14
|
+
|
15
|
+
b) use the modified software only within your corporation or
|
16
|
+
organization.
|
17
|
+
|
18
|
+
c) rename any non-standard executables so the names do not conflict
|
19
|
+
with standard executables, which must also be provided.
|
20
|
+
|
21
|
+
d) make other distribution arrangements with the author.
|
22
|
+
|
23
|
+
3. You may distribute the software in object code or executable
|
24
|
+
form, provided that you do at least ONE of the following:
|
25
|
+
|
26
|
+
a) distribute the executables and library files of the software,
|
27
|
+
together with instructions (in the manual page or equivalent)
|
28
|
+
on where to get the original distribution.
|
29
|
+
|
30
|
+
b) accompany the distribution with the machine-readable source of
|
31
|
+
the software.
|
32
|
+
|
33
|
+
c) give non-standard executables non-standard names, with
|
34
|
+
instructions on where to get the original software distribution.
|
35
|
+
|
36
|
+
d) make other distribution arrangements with the author.
|
37
|
+
|
38
|
+
4. You may modify and include the part of the software into any other
|
39
|
+
software (possibly commercial).
|
40
|
+
|
41
|
+
5. The scripts and library files supplied as input to or produced as
|
42
|
+
output from the software do not automatically fall under the
|
43
|
+
copyright of the software, but belong to whomever generated them,
|
44
|
+
and may be sold commercially, and may be aggregated with this
|
45
|
+
software.
|
46
|
+
|
47
|
+
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
48
|
+
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
49
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
50
|
+
PURPOSE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
== What is it?
|
2
|
+
Dabcup is a tool in order to handle databases backups easily. You can centralize
|
3
|
+
all you backup policies on a single server, and then store dumps to many different
|
4
|
+
hosts (SSH, S3, FTP).
|
5
|
+
|
6
|
+
== How does it work?
|
7
|
+
You just need to describe your policies in a simple configuration (see conf/dabcup.yml)
|
8
|
+
and then run the command: `dabcup foo dump` to backup your database 'foo'.
|
9
|
+
|
10
|
+
== How to install it?
|
11
|
+
You need Ruby plus the following gems: aws-s3 and net-ssh.
|
12
|
+
|
13
|
+
sudo apt-get install ruby rubygems
|
14
|
+
sudo gem install aws-s3 net-ssh
|
data/bin/dabcup
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
app_dir = Pathname.new(__FILE__).realpath().dirname().dirname()
|
6
|
+
lib_dir = File.join(app_dir, 'lib')
|
7
|
+
$LOAD_PATH << lib_dir
|
8
|
+
|
9
|
+
require 'yaml'
|
10
|
+
require 'dabcup'
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'rubygems'
|
14
|
+
rescue LoadError => ex
|
15
|
+
end
|
16
|
+
|
17
|
+
app = Dabcup::App.new(app_dir)
|
18
|
+
app.main(ARGV)
|
data/config/dabcup.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Sample config file for dabcup.
|
2
|
+
|
3
|
+
foo:
|
4
|
+
dump: pg_dump -U postgres foo -f %{dump_path}
|
5
|
+
restore: pg_restore -U postgres -d foo -f %{dump_path}
|
6
|
+
storage: file:///home/foo/dumps
|
7
|
+
spare_storage: s3://MY_KEY:MY_SECRET@MY_BUCKET.s3.amazonaws.com
|
8
|
+
retention: 10
|
9
|
+
|
10
|
+
bar:
|
11
|
+
tunnel: ssh://me@sql.my-site.com
|
12
|
+
dump: mysqldump my_db > %{dump_path}
|
13
|
+
storage: ftp://bar:password@backup.my-site.com/dumps
|
14
|
+
spare_storage: sftp://bar@other-backup.my-site.com:/home/bar/dumps
|
15
|
+
retention: 10
|
16
|
+
|
data/lib/dabcup.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Dabcup
|
2
|
+
def self.time_to_name(time)
|
3
|
+
time.strftime('%Y-%m-%dT%H:%M:%S') + '.dump'
|
4
|
+
end
|
5
|
+
|
6
|
+
class Error < StandardError
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'tmpdir'
|
11
|
+
|
12
|
+
require 'dabcup/app'
|
13
|
+
require 'dabcup/database'
|
14
|
+
require 'dabcup/storage'
|
15
|
+
require 'dabcup/operation'
|
16
|
+
require 'dabcup/help'
|
data/lib/dabcup/app.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Dabcup
|
4
|
+
class App
|
5
|
+
DABCUP_PATH = File.expand_path('~/.dabcup')
|
6
|
+
CONFIG_PATHS = ['dabcup.yml', '~/.dabcup/profiles.yml', '/etc/dabcup/profiles.yml'].freeze
|
7
|
+
|
8
|
+
attr_reader :config
|
9
|
+
attr_reader :profiles
|
10
|
+
|
11
|
+
|
12
|
+
def initialize(app_dir)
|
13
|
+
@app_dir = app_dir
|
14
|
+
end
|
15
|
+
|
16
|
+
def main(args)
|
17
|
+
if args.size < 1
|
18
|
+
puts "Try 'dabcup help'."
|
19
|
+
elsif ['help', '-h', '--help', '?'].include?(args[0])
|
20
|
+
help(args)
|
21
|
+
else
|
22
|
+
run(args)
|
23
|
+
end
|
24
|
+
#rescue Dabcup::Error => ex
|
25
|
+
# $stderr.puts ex.message
|
26
|
+
#rescue => ex
|
27
|
+
# $stderr.puts ex.message
|
28
|
+
# $stderr.puts 'See log for more informations.'
|
29
|
+
# Dabcup::fatal(ex)
|
30
|
+
end
|
31
|
+
|
32
|
+
def run(args)
|
33
|
+
database_name, operation_name = args[0 .. 1]
|
34
|
+
raise Dabcup::Error.new("Database '#{database_name}' doesn't exist.") unless config[database_name]
|
35
|
+
database = Database.new(database_name, config[database_name])
|
36
|
+
operation = Operation.build(operation_name, database)
|
37
|
+
operation.run(args)
|
38
|
+
ensure
|
39
|
+
operation.terminate if operation
|
40
|
+
end
|
41
|
+
|
42
|
+
def help(args)
|
43
|
+
puts Dabcup::Help.message(args[1])
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def load_yaml(file_path)
|
49
|
+
File.open(File.expand_path(file_path)) do |stream| YAML.load(stream) end
|
50
|
+
end
|
51
|
+
|
52
|
+
def config_path
|
53
|
+
CONFIG_PATHS.find { |path| File.exists?(path) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def config
|
57
|
+
@config ||= YAML.load_file(config_path)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'net/ssh'
|
3
|
+
|
4
|
+
module Dabcup
|
5
|
+
class Database
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :config
|
8
|
+
attr_reader :main_storage
|
9
|
+
attr_reader :spare_storage
|
10
|
+
|
11
|
+
def initialize(name, config)
|
12
|
+
@name = name
|
13
|
+
@config = config
|
14
|
+
@main_storage = Dabcup::Storage.new(config['storage'])
|
15
|
+
@spare_storage = Dabcup::Storage.new(config['spare_storage']) if config['spare_storage']
|
16
|
+
extend(Tunnel) if tunnel
|
17
|
+
end
|
18
|
+
|
19
|
+
def tunnel
|
20
|
+
@tunnel ||= Addressable::URI.parse(config['tunnel']) if config['tunnel']
|
21
|
+
end
|
22
|
+
|
23
|
+
def via_ssh?
|
24
|
+
tunnel != nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def dump(dump_path)
|
28
|
+
system(config['dump'], :dump_path => File.expand_path(dump_path))
|
29
|
+
end
|
30
|
+
|
31
|
+
def system(command, interpolation = {})
|
32
|
+
command = command % interpolation
|
33
|
+
# TODO Found a nice way to get the exit status.
|
34
|
+
stdin, stdout, stderr = Open3.popen3(command + "; echo $?")
|
35
|
+
raise Dabcup::Error.new("Failed to execute '#{command}', stderr is '#{stderr.read}'.") if not stderr.eof?
|
36
|
+
[stdin, stdout, stderr]
|
37
|
+
end
|
38
|
+
|
39
|
+
def retention
|
40
|
+
config['retention']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Tunnel
|
45
|
+
def system(command, interpolation = {})
|
46
|
+
command = command % interpolation
|
47
|
+
stdout = ssh.exec!(command)
|
48
|
+
end
|
49
|
+
|
50
|
+
def ssh
|
51
|
+
@ssh ||= Net::SSH.start(tunnel.host, tunnel.user, :password => tunnel.password)
|
52
|
+
end
|
53
|
+
|
54
|
+
def disconnect
|
55
|
+
@ssh.close if @ssh
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
data/lib/dabcup/help.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
class Dabcup::Help
|
2
|
+
def self.message(name)
|
3
|
+
@@messages[name]
|
4
|
+
end
|
5
|
+
|
6
|
+
default_help = <<__HELP__
|
7
|
+
Usage:
|
8
|
+
dabcup <profile> <operation> [parameters]
|
9
|
+
|
10
|
+
Operations:
|
11
|
+
clean
|
12
|
+
clear
|
13
|
+
delete
|
14
|
+
get
|
15
|
+
list
|
16
|
+
populate
|
17
|
+
restore
|
18
|
+
store
|
19
|
+
|
20
|
+
Try 'dabcup help <operation>' to get details.
|
21
|
+
Visit http://dabcup.obloh.com for more informations.
|
22
|
+
__HELP__
|
23
|
+
|
24
|
+
@@messages = Hash.new(default_help)
|
25
|
+
|
26
|
+
@@messages['clean'] = <<__HELP__
|
27
|
+
Removes old dumps from the storage and the spare storage. Clean rules are
|
28
|
+
specified int the 'keep' section of your profile.
|
29
|
+
|
30
|
+
my-profile:
|
31
|
+
keep:
|
32
|
+
days_of_week: 1 # Keeps all dumps of the first day of the week
|
33
|
+
less_days_than: 50 # Keep dumps younger than 50 days
|
34
|
+
|
35
|
+
dabcup <profile> clean
|
36
|
+
__HELP__
|
37
|
+
|
38
|
+
@@messages['clear'] = <<__HELP__
|
39
|
+
Delete all dumps from the main and the spare storages. Use safely this operation
|
40
|
+
because no confirmation is asked.
|
41
|
+
|
42
|
+
dabcup <profile> clear
|
43
|
+
__HELP__
|
44
|
+
|
45
|
+
@@messages['delete'] = <<__HELP__
|
46
|
+
Deletes the specified dump.
|
47
|
+
|
48
|
+
dabcup <profile> <dump>
|
49
|
+
__HELP__
|
50
|
+
|
51
|
+
@@messages['get'] = <<__HELP__
|
52
|
+
Retrieves the specified dump from the main or the spare storage. The local path
|
53
|
+
represents where you want to retrieve the dump. If not specified the dump will
|
54
|
+
be downloaded into the current directory.
|
55
|
+
|
56
|
+
dabcup <profile> get <dump> [<local_path>]
|
57
|
+
__HELP__
|
58
|
+
|
59
|
+
@@messages['list'] = <<__HELP__
|
60
|
+
Lists dumps of the both storages. The flags 'M' and 'S' means if the dump is
|
61
|
+
in the main and/or the spare storage.
|
62
|
+
|
63
|
+
dabcup <profile> list
|
64
|
+
__HELP__
|
65
|
+
|
66
|
+
@@messages['populate'] = <<__HELP__
|
67
|
+
The purpose of this operation is only to help you to test clean rules. It
|
68
|
+
populates the main and the spare storages with n backups. Each backup get back a
|
69
|
+
day before.
|
70
|
+
|
71
|
+
dabcup <profile> populate <n>
|
72
|
+
__HELP__
|
73
|
+
|
74
|
+
@@messages['restore'] = <<__HELP__
|
75
|
+
Download the specified dump from the storage, or the spare storage,
|
76
|
+
and restore it to the database. Use 'list' operation to see dumps.
|
77
|
+
|
78
|
+
dabcup <profile> restore <dump>
|
79
|
+
__HELP__
|
80
|
+
|
81
|
+
@@messages['store'] = <<__HELP__
|
82
|
+
Dump the database, and upload it to the storage, and sare storage it set.
|
83
|
+
|
84
|
+
dabcup <profile> store
|
85
|
+
__HELP__
|
86
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'dabcup/operation/base'
|
2
|
+
require 'dabcup/operation/clean'
|
3
|
+
require 'dabcup/operation/clear'
|
4
|
+
require 'dabcup/operation/remove'
|
5
|
+
require 'dabcup/operation/get'
|
6
|
+
require 'dabcup/operation/list'
|
7
|
+
require 'dabcup/operation/populate'
|
8
|
+
require 'dabcup/operation/restore'
|
9
|
+
require 'dabcup/operation/dump'
|
10
|
+
|
11
|
+
module Dabcup
|
12
|
+
module Operation
|
13
|
+
def self.build(name, config)
|
14
|
+
case name
|
15
|
+
when 'dump' then Dabcup::Operation::Dump.new(config)
|
16
|
+
when 'restore' then Dabcup::Operation::Restore.new(config)
|
17
|
+
when 'list' then Dabcup::Operation::List.new(config)
|
18
|
+
when 'get' then Dabcup::Operation::Get.new(config)
|
19
|
+
when 'remove' then Dabcup::Operation::Remove.new(config)
|
20
|
+
when 'clear' then Dabcup::Operation::Clear.new(config)
|
21
|
+
when 'clean' then Dabcup::Operation::Clean.new(config)
|
22
|
+
when 'populate' then Dabcup::Operation::Populate.new(config)
|
23
|
+
when 'test' then Dabcup::Operation::Test.new(config)
|
24
|
+
else
|
25
|
+
raise Dabcup::Error.new("Unknow operation '#{name}'.")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class Base
|
4
|
+
attr_reader :database
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :database, :main_storage, :spare_storage
|
7
|
+
|
8
|
+
def initialize(database)
|
9
|
+
@database = database
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
raise NotImplementedError.new("Sorry")
|
14
|
+
end
|
15
|
+
|
16
|
+
def terminate
|
17
|
+
main_storage.disconnect if main_storage
|
18
|
+
spare_storage.disconnect if spare_storage
|
19
|
+
end
|
20
|
+
|
21
|
+
# Try to returns the best directory path to dump the database.
|
22
|
+
def best_dumps_path
|
23
|
+
if database.via_ssh?
|
24
|
+
return main_storage.path if same_ssh_as_database?(main_storage)
|
25
|
+
else
|
26
|
+
return main_storage.path if main_storage.local?
|
27
|
+
end
|
28
|
+
Dir.tmpdir
|
29
|
+
end
|
30
|
+
|
31
|
+
# Try to returns the best local directory path.
|
32
|
+
def best_local_dumps_path
|
33
|
+
return spare_storage.path if spare_storage.local?
|
34
|
+
Dir.tmpdir
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_local_dump?
|
38
|
+
!main_storage.local? && (spare_storage && !spare_storage.local?)
|
39
|
+
end
|
40
|
+
|
41
|
+
def same_ssh_as_database?(storage)
|
42
|
+
return false if not storage.driver.is_a?(Dabcup::Storage::Driver::SFTP)
|
43
|
+
storage.driver.host == database.tunnel.host
|
44
|
+
end
|
45
|
+
|
46
|
+
def check
|
47
|
+
return if not database.via_ssh?
|
48
|
+
if not same_ssh_as_database?(main_storage)
|
49
|
+
raise Error.new("When dumping via SSH the main storage must be local to the database.")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class Clean < Base
|
4
|
+
def run(args)
|
5
|
+
clean_storage(main_storage)
|
6
|
+
clean_storage(spare_storage) if spare_storage
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def clean_storage(storage)
|
12
|
+
if (retention = database.retention.to_i) < 1
|
13
|
+
raise Error.new("Retention must be greater than zero")
|
14
|
+
end
|
15
|
+
storage.delete(storage.list[0 .. -retention-1])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class Dump < Base
|
4
|
+
def run(args)
|
5
|
+
database.dump(dump_path)
|
6
|
+
copy_dump_to_main_storage
|
7
|
+
copy_dump_to_spare_storage
|
8
|
+
ensure
|
9
|
+
path = local_dump_path || dump_path
|
10
|
+
File.delete(path) if remove_local_dump? && File.exists?(path)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def copy_dump_to_main_storage
|
16
|
+
unless main_storage.exists?(dump_name)
|
17
|
+
retrieve_dump_from_remote_database if retrieve_dump_from_remote_database?
|
18
|
+
main_storage.put(dump_path, dump_name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def copy_dump_to_spare_storage
|
23
|
+
if spare_storage && !spare_storage.exists?(dump_name)
|
24
|
+
main_storage.get(dump_name, local_dump_path) unless File.exists?(local_dump_path)
|
25
|
+
spare_storage.put(local_dump_path, dump_name) unless spare_storage.exists?(dump_name)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def dump_name
|
30
|
+
@dump_name ||= database.name + '_' + Dabcup::time_to_name(Time.now)
|
31
|
+
end
|
32
|
+
|
33
|
+
def dump_path
|
34
|
+
@dump_path ||= File.join(best_dumps_path, dump_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def local_dump_path
|
38
|
+
File.exists?(dump_path) ? dump_path : File.join(best_local_dumps_path, dump_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def retrieve_dump_from_remote_database
|
42
|
+
Storage::Driver.build(database.tunnel.to_s + '/tmp').get(dump_name, local_dump_path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def retrieve_dump_from_remote_database?
|
46
|
+
database.via_ssh? && !same_ssh_as_database?(main_storage)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class Get < Base
|
4
|
+
def run(args)
|
5
|
+
raise Dabcup::Error.new("Not enough arguments. Try 'dabcup help get'.") if args.size < 3
|
6
|
+
dump_name = args[2]
|
7
|
+
local_path = args[3] || dump_name
|
8
|
+
local_path = File.join(local_path, dump_name) if File.directory?(local_path)
|
9
|
+
local_path = File.expand_path(local_path)
|
10
|
+
if main_storage.exists?(dump_name)
|
11
|
+
main_storage.get(dump_name, local_path)
|
12
|
+
elsif spare_storage.exists?(dump_name)
|
13
|
+
spare_storage.get(dump_name, local_path)
|
14
|
+
else
|
15
|
+
raise Dabcup::Error.new("Dump '#{dump_name}' not found.")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class List < Base
|
4
|
+
def run(args)
|
5
|
+
max_length = 0
|
6
|
+
main_dumps = main_storage.list
|
7
|
+
spare_dumps = spare_storage ? spare_storage.list : []
|
8
|
+
# Intersection of main_dumps and spare_dumps
|
9
|
+
dumps = main_dumps + spare_dumps.select do |dump| not main_dumps.include?(dump) end
|
10
|
+
# Get length of the longest name
|
11
|
+
max_length= (dumps.map {|d| d.name}.max {|l, r| l <=> r} || '').size
|
12
|
+
# Prints names, sizes and flags
|
13
|
+
dumps.each do |dump|
|
14
|
+
name_str = dump.name.ljust(max_length + 2)
|
15
|
+
size_str = (dump.size / 1024).to_s.rjust(8)
|
16
|
+
location = main_dumps.include?(dump) ? 'M' : ' '
|
17
|
+
location += spare_dumps.include?(dump) ? 'S' : ' '
|
18
|
+
puts "#{name_str}#{size_str} KB #{location}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class Populate < Base
|
4
|
+
def run(args)
|
5
|
+
now = Time.now
|
6
|
+
days_before = args[2].to_i
|
7
|
+
local_file_name = Dabcup::Database::dump_name(database)
|
8
|
+
local_file_path = File.join(Dir.tmpdir, local_file_name)
|
9
|
+
database.dump(local_file_path)
|
10
|
+
for day_before in (0 .. days_before)
|
11
|
+
remote_file_name = Dabcup::Database::dump_name(database, now - (day_before * 24 * 3600))
|
12
|
+
main_storage.put(local_file_path, remote_file_name)
|
13
|
+
spare_storage.put(local_file_path, remote_file_name) if spare_storage
|
14
|
+
end
|
15
|
+
ensure
|
16
|
+
File.delete(local_file_path) if local_file_path and File.exists?(local_file_path)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class Remove < Base
|
4
|
+
def run(args)
|
5
|
+
raise Dabcup::Error.new("Not enough arguments. Try 'dabcup help delete'") if args.size < 3
|
6
|
+
remove_from_storage(main_storage, args[2])
|
7
|
+
remove_from_storage(spare_storage, args[2])
|
8
|
+
end
|
9
|
+
|
10
|
+
def remove_from_storage(storage, name)
|
11
|
+
storage.delete(name) if storage.exists?(name)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Dabcup
|
2
|
+
module Operation
|
3
|
+
class Restore < Base
|
4
|
+
def run(args)
|
5
|
+
database.via_ssh? ? restore_with_ssh(args) : restore_without_ssh(args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def restore_without_ssh(args)
|
9
|
+
raise Dabcup::Error.new("Not enough arguments. Try 'dabcup help restore'") if args.size < 3
|
10
|
+
dump_name = args[2]
|
11
|
+
dump_path = File.join(Dir.tmpdir, dump_name)
|
12
|
+
if main_storage.exists?(dump_name)
|
13
|
+
main_storage.get(dump_name, dump_path)
|
14
|
+
elsif spare_storage and spare_storage.exists?(dump_name)
|
15
|
+
spare_storage.get(dump_name, dump_path)
|
16
|
+
else
|
17
|
+
raise Dabcup::Error.new("Dump '#{dump_name}' not found.")
|
18
|
+
end
|
19
|
+
database.restore(dump_path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def retore_with_ssh(args)
|
23
|
+
raise Dabcup::Error.new("Not enough arguments. Try 'dabcup help restore'") if args.size < 3
|
24
|
+
dump_name = args[2]
|
25
|
+
dump_path = File.join(main_storage.path, dump_name)
|
26
|
+
local_dump_path = nil
|
27
|
+
if not main_storage.exists?(dump_name)
|
28
|
+
if spare_storage.is_a?(Dabcup::Storage::Local)
|
29
|
+
local_dump_path = File.join(spare_storage.path, dump_name)
|
30
|
+
else
|
31
|
+
spare_storage.get(dump_name, local_dump_path)
|
32
|
+
end
|
33
|
+
main_storage.put(dump_path, dump_name) if not main_storage.exists?(dump_name)
|
34
|
+
end
|
35
|
+
database.restore(dump_path)
|
36
|
+
ensure
|
37
|
+
#File.delete(local_dump_path) if local_dump_path and File.exists?(local_dump_path)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'net/ftp'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
require 'dabcup/storage/driver/base'
|
6
|
+
require 'dabcup/storage/driver/local'
|
7
|
+
require 'dabcup/storage/driver/sftp'
|
8
|
+
require 'dabcup/storage/driver/ftp'
|
9
|
+
require 'dabcup/storage/driver/s3'
|
10
|
+
|
11
|
+
require 'dabcup/storage/dump'
|
12
|
+
|
13
|
+
module Dabcup
|
14
|
+
class Storage
|
15
|
+
attr_reader :rules, :driver
|
16
|
+
|
17
|
+
def initialize(url)
|
18
|
+
@driver = Driver.build(url)
|
19
|
+
end
|
20
|
+
|
21
|
+
def path
|
22
|
+
@driver.path
|
23
|
+
end
|
24
|
+
|
25
|
+
def disconnect
|
26
|
+
@driver.disconnect
|
27
|
+
end
|
28
|
+
|
29
|
+
def local?
|
30
|
+
@driver.local?
|
31
|
+
end
|
32
|
+
|
33
|
+
def put(local_path, remote_name)
|
34
|
+
@driver.put(local_path, remote_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get(remote_name, local_path)
|
38
|
+
@driver.get(remote_name, local_path)
|
39
|
+
end
|
40
|
+
|
41
|
+
def list
|
42
|
+
dumps = @driver.list.inject([]) { |array, dump| dump.valid? ? array << dump : array }
|
43
|
+
dumps.sort { |left, right| left.created_at <=> right.created_at }
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(dump_or_string_or_array)
|
47
|
+
file_names = array_of_dumps_names(dump_or_string_or_array)
|
48
|
+
file_names.each { |file_name| @driver.delete(file_name) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def clear
|
52
|
+
delete(list)
|
53
|
+
end
|
54
|
+
|
55
|
+
def exists?(name)
|
56
|
+
list.any? { |dump| dump.name == name }
|
57
|
+
end
|
58
|
+
|
59
|
+
def dump_name?(name)
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_by_name(file_name)
|
64
|
+
list.find { |dump| dump.name == file_name }
|
65
|
+
end
|
66
|
+
|
67
|
+
def name
|
68
|
+
"#{@login}@#{@host}:#{port}:#{@path}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_port(port)
|
72
|
+
@port = port if @port.nil? or @port.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns an array of String representing dumps names.
|
76
|
+
# If the argument is an array it must contains only String or Dump objects.
|
77
|
+
def array_of_dumps_names(dump_or_string_or_array)
|
78
|
+
case dump_or_string_or_array
|
79
|
+
when String
|
80
|
+
[dump_or_string_or_array]
|
81
|
+
when Dump
|
82
|
+
[dump_or_string_or_array.name]
|
83
|
+
when Array
|
84
|
+
dump_or_string_or_array.map do |dump_or_string|
|
85
|
+
case dump_or_string
|
86
|
+
when String
|
87
|
+
dump_or_string
|
88
|
+
when Dump
|
89
|
+
dump_or_string.name
|
90
|
+
else
|
91
|
+
raise ArgumentError.new("Expecting an array of String or Dump instead of #{dump_or_string.class}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
else
|
95
|
+
raise ArgumentError.new("Expecting a String or Dump or and Array instead of #{dump_or_string_or_array.class}")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Dabcup
|
5
|
+
class Storage
|
6
|
+
module Driver
|
7
|
+
def self.build(url)
|
8
|
+
if url.include?('file://')
|
9
|
+
Dabcup::Storage::Driver::Local.new(url)
|
10
|
+
elsif url.include?('ssh://')
|
11
|
+
Dabcup::Storage::Driver::SFTP.new(url)
|
12
|
+
elsif url.include?('ftp://')
|
13
|
+
Dabcup::Storage::Driver::FTP.new(url)
|
14
|
+
elsif url.include?('s3://')
|
15
|
+
Dabcup::Storage::Driver::S3.new(url)
|
16
|
+
elsif url.include?('ftp://')
|
17
|
+
Dabcup::Storage::Driver::FTP.new(url)
|
18
|
+
else
|
19
|
+
raise "No driver found for '#{url}'"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Base
|
24
|
+
attr_reader :uri
|
25
|
+
extend Forwardable
|
26
|
+
def_delegators :@uri, :host, :port, :user, :password, :path
|
27
|
+
|
28
|
+
def initialize(uri)
|
29
|
+
@uri = Addressable::URI.parse(uri)
|
30
|
+
end
|
31
|
+
|
32
|
+
################################
|
33
|
+
##### Methods to implement #####
|
34
|
+
################################
|
35
|
+
|
36
|
+
def disconnect
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def put(local_path, remote_name)
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(remote_name, local_path)
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def list
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(dump_name)
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
def protocol
|
57
|
+
raise NotImplementedError
|
58
|
+
end
|
59
|
+
|
60
|
+
def local?
|
61
|
+
raise NotImplementedError
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Dabcup
|
2
|
+
class Storage
|
3
|
+
module Driver
|
4
|
+
class FTP < Base
|
5
|
+
def put(local_path, remote_name)
|
6
|
+
remote_path = File.join(path, remote_name)
|
7
|
+
ftp.putbinaryfile(local_path, remote_path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(remote_name, local_path)
|
11
|
+
remote_path = File.join(path, remote_name)
|
12
|
+
ftp.getbinaryfile(remote_path, local_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def list
|
16
|
+
dumps = []
|
17
|
+
lines = ftp.list(path)
|
18
|
+
lines.collect do |str|
|
19
|
+
fields = str.split(' ')
|
20
|
+
next unless Dump.valid_name?(fields[8])
|
21
|
+
dumps << Dabcup::Storage::Dump.new(:name => fields[8], :size => fields[4].to_i)
|
22
|
+
end
|
23
|
+
dumps
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete(file_name)
|
27
|
+
file_path = File.join(path, file_name)
|
28
|
+
ftp.delete(file_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def ftp
|
32
|
+
unless @ftp
|
33
|
+
@ftp = Net::FTP.new
|
34
|
+
@ftp.connect(host, port || 21)
|
35
|
+
@ftp.login(user, password)
|
36
|
+
mkdirs
|
37
|
+
end
|
38
|
+
@ftp
|
39
|
+
end
|
40
|
+
|
41
|
+
def disconnect
|
42
|
+
@ftp.close if @ftp
|
43
|
+
end
|
44
|
+
|
45
|
+
def local?
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
def mkdirs
|
50
|
+
dirs = []
|
51
|
+
path = path
|
52
|
+
first_exception = nil
|
53
|
+
begin
|
54
|
+
ftp.nlst(path)
|
55
|
+
rescue Net::FTPTempError => ex
|
56
|
+
dirs << path
|
57
|
+
path = File.dirname(path)
|
58
|
+
first_exception = ex unless first_exception
|
59
|
+
if path == '.'
|
60
|
+
raise first_exception
|
61
|
+
else
|
62
|
+
retry
|
63
|
+
end
|
64
|
+
end
|
65
|
+
dirs.reverse.each do |dir|
|
66
|
+
ftp.mkdir(dir)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Dabcup
|
2
|
+
class Storage
|
3
|
+
module Driver
|
4
|
+
class Local < Base
|
5
|
+
def protocol
|
6
|
+
'file'
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(url)
|
10
|
+
super(url)
|
11
|
+
@path = File.expand_path(url.sub('file://', ''))
|
12
|
+
end
|
13
|
+
|
14
|
+
def put(local_path, remote_name)
|
15
|
+
remote_path = File.join(@path, remote_name)
|
16
|
+
FileUtils.copy(local_path, remote_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(remote_name, local_path)
|
20
|
+
connect
|
21
|
+
remote_path = File.join(@path, remote_name)
|
22
|
+
FileUtils.copy(remote_path, local_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def list
|
26
|
+
dumps = []
|
27
|
+
Dir.foreach(@path) do |name|
|
28
|
+
dumps << Dump.new(:name => name, :size => File.size(File.join(@path, name)))
|
29
|
+
end
|
30
|
+
dumps
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete(file_name)
|
34
|
+
file_path = File.join(@path, file_name)
|
35
|
+
File.delete(file_path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def connect
|
39
|
+
FileUtils.mkpath(@path) if not File.exist?(@path)
|
40
|
+
raise DabcupError.new("The path '#{@path}' is not a directory.") if not File.directory?(@path)
|
41
|
+
end
|
42
|
+
|
43
|
+
def disconnect
|
44
|
+
end
|
45
|
+
|
46
|
+
def local?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Dabcup
|
2
|
+
class Storage
|
3
|
+
module Driver
|
4
|
+
# Amazon S3
|
5
|
+
class S3 < Base
|
6
|
+
def initialize(uri)
|
7
|
+
super(uri)
|
8
|
+
require 'aws/s3'
|
9
|
+
rescue LoadError => ex
|
10
|
+
raise Dabcup::Error.new("The library aws-s3 is missing. Get it via 'gem install aws-s3' and set RUBYOPT=rubygems.")
|
11
|
+
end
|
12
|
+
|
13
|
+
def protocol
|
14
|
+
's3'
|
15
|
+
end
|
16
|
+
|
17
|
+
def put(local_path, remote_path)
|
18
|
+
connect
|
19
|
+
File.open(local_path) do |file|
|
20
|
+
AWS::S3::S3Object.store(remote_path, file, bucket)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(remote_path, local_path)
|
25
|
+
connect
|
26
|
+
File.open(local_path, 'w') do |file|
|
27
|
+
AWS::S3::S3Object.stream(remote_path, bucket) do |stream|
|
28
|
+
file.write(stream)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def list
|
34
|
+
connect
|
35
|
+
AWS::S3::Bucket.find(bucket).objects.collect do |obj|
|
36
|
+
Dump.new(:name => obj.key.to_s, :size => obj.size)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(file_name)
|
41
|
+
connect
|
42
|
+
AWS::S3::S3Object.delete(file_name, bucket)
|
43
|
+
end
|
44
|
+
|
45
|
+
def connect
|
46
|
+
return if AWS::S3::Base.connected?
|
47
|
+
AWS::S3::Base.establish_connection!(:access_key_id => uri.user, :secret_access_key => uri.password)
|
48
|
+
create_bucket
|
49
|
+
end
|
50
|
+
|
51
|
+
def disconnect
|
52
|
+
AWS::S3::Base.disconnect!
|
53
|
+
end
|
54
|
+
|
55
|
+
def bucket
|
56
|
+
@bucket ||= uri.host.split('.').first
|
57
|
+
end
|
58
|
+
|
59
|
+
def local?
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_bucket
|
64
|
+
AWS::S3::Bucket.list.each do |bucket|
|
65
|
+
return if bucket.name == bucket
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require('net/sftp')
|
2
|
+
|
3
|
+
module Dabcup
|
4
|
+
class Storage
|
5
|
+
module Driver
|
6
|
+
class SFTP < Base
|
7
|
+
def protocol
|
8
|
+
'sftp'
|
9
|
+
end
|
10
|
+
|
11
|
+
def put(local_path, remote_name)
|
12
|
+
remote_path = File.join(uri.path, remote_name)
|
13
|
+
sftp.upload!(local_path, remote_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(remote_name, local_path)
|
17
|
+
remote_path = File.join(uri.path, remote_name)
|
18
|
+
sftp.download!(remote_path, local_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def list
|
22
|
+
dumps = []
|
23
|
+
handle = sftp.opendir!(uri.path)
|
24
|
+
while 1
|
25
|
+
request = sftp.readdir(handle).wait
|
26
|
+
break if request.response.eof?
|
27
|
+
raise Dabcup::Error.new("Failed to list files from #{@login}@#{@host}:#{uri.path}") unless request.response.ok?
|
28
|
+
request.response.data[:names].each do |file|
|
29
|
+
dumps << Dump.new(:name => file.name, :size => file.attributes.size)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
dumps
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(file_name)
|
36
|
+
file_path = File.join(uri.path, file_name)
|
37
|
+
sftp.remove!(file_path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def disconnect
|
41
|
+
@sftp.close(nil) if @sftp
|
42
|
+
end
|
43
|
+
|
44
|
+
def local?
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create directories if necessary
|
49
|
+
def mkdirs
|
50
|
+
dirs = []
|
51
|
+
path = uri.path
|
52
|
+
first_exception = nil
|
53
|
+
# TODO: find an exists? method
|
54
|
+
begin
|
55
|
+
sftp.dir.entries(path)
|
56
|
+
rescue Net::SFTP::StatusException => ex
|
57
|
+
dirs << path
|
58
|
+
path = File.dirname(path)
|
59
|
+
first_exception ||= ex
|
60
|
+
if path == '.'
|
61
|
+
raise first_exception
|
62
|
+
else
|
63
|
+
retry
|
64
|
+
end
|
65
|
+
end
|
66
|
+
dirs.reverse.each { |dir| sftp.mkdir!(dir) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def sftp
|
70
|
+
unless @sftp
|
71
|
+
@sftp = Net::SFTP.start(uri.host, uri.user, :password => uri.password)
|
72
|
+
@sftp.connect
|
73
|
+
mkdirs
|
74
|
+
end
|
75
|
+
@sftp
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Dabcup
|
2
|
+
class Storage
|
3
|
+
class Dump
|
4
|
+
IGNORED_NAMES = %w(. ..).freeze
|
5
|
+
|
6
|
+
attr_accessor :name
|
7
|
+
attr_accessor :size
|
8
|
+
|
9
|
+
def self.valid_name?(name)
|
10
|
+
!IGNORED_NAMES.include?(name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(attrs = {})
|
14
|
+
self.name = attrs[:name]
|
15
|
+
self.size = attrs[:size]
|
16
|
+
end
|
17
|
+
|
18
|
+
def created_at
|
19
|
+
Time.parse(name)
|
20
|
+
rescue ArgumentError
|
21
|
+
nil # Invalid date => ignore file name
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(dump)
|
25
|
+
dump && name == dump.name && size == dump.size
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?
|
29
|
+
self.class.valid_name?(name) && created_at != nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dabcup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.2
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alexis Bernard
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-08 00:00:00 +02:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: aws-s3
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.6.2
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-sftp
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 2.0.5
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: addressable
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 2.2.5
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id003
|
49
|
+
description: You can centralize all you backup policies on a single server, and then store dumps to many different hosts (SSH, S3, FTP).
|
50
|
+
email: alexis@obloh.com
|
51
|
+
executables:
|
52
|
+
- dabcup
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files: []
|
56
|
+
|
57
|
+
files:
|
58
|
+
- bin/dabcup
|
59
|
+
- lib/dabcup.rb
|
60
|
+
- lib/dabcup/storage/driver/ftp.rb
|
61
|
+
- lib/dabcup/storage/driver/sftp.rb
|
62
|
+
- lib/dabcup/storage/driver/local.rb
|
63
|
+
- lib/dabcup/storage/driver/s3.rb
|
64
|
+
- lib/dabcup/storage/driver/base.rb
|
65
|
+
- lib/dabcup/storage/dump.rb
|
66
|
+
- lib/dabcup/help.rb
|
67
|
+
- lib/dabcup/operation/get.rb
|
68
|
+
- lib/dabcup/operation/restore.rb
|
69
|
+
- lib/dabcup/operation/dump.rb
|
70
|
+
- lib/dabcup/operation/clean.rb
|
71
|
+
- lib/dabcup/operation/remove.rb
|
72
|
+
- lib/dabcup/operation/list.rb
|
73
|
+
- lib/dabcup/operation/clear.rb
|
74
|
+
- lib/dabcup/operation/base.rb
|
75
|
+
- lib/dabcup/operation/populate.rb
|
76
|
+
- lib/dabcup/storage.rb
|
77
|
+
- lib/dabcup/operation.rb
|
78
|
+
- lib/dabcup/database.rb
|
79
|
+
- lib/dabcup/app.rb
|
80
|
+
- config/dabcup.yml
|
81
|
+
- LICENSE
|
82
|
+
- README.rdoc
|
83
|
+
has_rdoc: true
|
84
|
+
homepage: http://dabcup.rubyforge.org/
|
85
|
+
licenses: []
|
86
|
+
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: 1.8.7
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: "0"
|
104
|
+
requirements: []
|
105
|
+
|
106
|
+
rubyforge_project: dabcup
|
107
|
+
rubygems_version: 1.6.2
|
108
|
+
signing_key:
|
109
|
+
specification_version: 3
|
110
|
+
summary: Dabcup is a tool in order to handle databases backups easily
|
111
|
+
test_files: []
|
112
|
+
|