dabcup 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|