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 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
+
@@ -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,10 @@
1
+ module Dabcup
2
+ module Operation
3
+ class Clear < Base
4
+ def run(args)
5
+ main_storage.clear
6
+ spare_storage.clear if spare_storage
7
+ end
8
+ end
9
+ end
10
+ 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
+