dabcup 0.1.2

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