outback 0.0.14 → 1.1.0

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +11 -0
  3. data/LICENSE +21 -0
  4. data/README.md +29 -3
  5. data/lib/outback/archive.rb +6 -17
  6. data/lib/outback/backup.rb +38 -20
  7. data/lib/outback/cli.rb +6 -2
  8. data/lib/outback/configuration.rb +15 -10
  9. data/lib/outback/directory_source.rb +8 -7
  10. data/lib/outback/directory_target.rb +18 -11
  11. data/lib/outback/encryption_processor.rb +34 -0
  12. data/lib/outback/errors.rb +7 -0
  13. data/lib/outback/logging.rb +7 -0
  14. data/lib/outback/mysql_source.rb +9 -9
  15. data/lib/outback/processor.rb +17 -0
  16. data/lib/outback/s3_target.rb +18 -9
  17. data/lib/outback/sftp_target.rb +70 -0
  18. data/lib/outback/source.rb +9 -2
  19. data/lib/outback/source_archive.rb +17 -0
  20. data/lib/outback/support/attr_setter.rb +1 -1
  21. data/lib/outback/support/configurable.rb +5 -3
  22. data/lib/outback/target.rb +52 -14
  23. data/lib/outback/target_archive.rb +30 -0
  24. data/lib/outback/version.rb +3 -0
  25. data/lib/outback.rb +16 -11
  26. data/lib/vendor/enumerable_ext.rb +9 -0
  27. data/lib/{outback/vendor → vendor}/metaclass.rb +1 -1
  28. data/lib/vendor/methodphitamine.rb +28 -0
  29. data/lib/vendor/mysql/charset.rb +325 -0
  30. data/lib/vendor/mysql/constants.rb +165 -0
  31. data/lib/vendor/mysql/error.rb +989 -0
  32. data/lib/vendor/mysql/packet.rb +78 -0
  33. data/lib/vendor/mysql/protocol.rb +770 -0
  34. data/lib/vendor/mysql.rb +1093 -0
  35. data/lib/vendor/numeric_ext.rb +49 -0
  36. data/lib/vendor/string_ext.rb +19 -0
  37. metadata +84 -43
  38. data/MIT-LICENSE +0 -20
  39. data/VERSION +0 -1
  40. data/lib/outback/configuration_error.rb +0 -4
  41. data/lib/outback/directory_archive.rb +0 -8
  42. data/lib/outback/local_archive.rb +0 -6
  43. data/lib/outback/s3_archive.rb +0 -18
  44. data/lib/outback/temp_archive.rb +0 -5
  45. data/lib/outback/vendor/methodphitamine.rb +0 -25
  46. data/lib/outback/vendor/mysql.rb +0 -1214
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4503e45304f0be857fab9999cddf991b4a3823668951f0ee476eb94c80abd420
4
+ data.tar.gz: 49451b535df2adcf44c66089101de73cd38fd9d4494cda5503db130bca36674e
5
+ SHA512:
6
+ metadata.gz: b88bc0e63f3145bf2d84e032c725e01de590f3d8560de0cb4d60b064895c81aa63827614f729b95d86c89282b106dbcb76afe6f7a9281fbaaef6449ecb937a37
7
+ data.tar.gz: 1dd915c8b1bf994bd7f78de43f834f4b3b1424da88636c788de8708fb30ca8b9ad05827dbd236e7070013e00484cd61d18ef0301186f870ffa0529f991ed9fed
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ *1.1.0 (July 8, 2024)*
2
+
3
+ * Support Ruby 3
4
+ * Do not fail on store or purge
5
+
6
+ *1.0.2 (September 20, 2016)*
7
+
8
+ * Add SCP target
9
+ * Add encryption with OpenSSL
10
+ * Drop activesupport dependency
11
+
1
12
  *0.0.12 (August 16, 2012)*
2
13
 
3
14
  * Fix warning caused by methodphitamine.rb
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 onrooby
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -28,7 +28,7 @@ instantiated during a single run.
28
28
 
29
29
  ```` ruby
30
30
  Outback::Configuration.new 'name' do
31
- source :directory, '/ver/www' do
31
+ source :directory, '/var/www' do
32
32
  exclude '/var/www/foo'
33
33
  exclude '/var/www/icons/*.png'
34
34
  end
@@ -45,6 +45,13 @@ Outback::Configuration.new 'name' do
45
45
  # database 'specific_database'
46
46
  end
47
47
 
48
+ processor :encryption do
49
+ password 'very secure encryption password'
50
+
51
+ # The default cipher is aes-256-cbc
52
+ #cipher 'openssl supported cipher'
53
+ end
54
+
48
55
  # Amazon S3 storage
49
56
  target :s3 do
50
57
  access_key 'S3 access key'
@@ -56,6 +63,19 @@ Outback::Configuration.new 'name' do
56
63
  # Just omit the definition to keep archives forever.
57
64
  ttl 1.month
58
65
  end
66
+
67
+ # SFTP storage
68
+ target :sftp, 'example.com' do
69
+ user 'backupuser'
70
+ # Usually, SSH keys will be used to connect to the server.
71
+ # Alternatively, a password can be given here.
72
+ #password 'your password'
73
+
74
+ # Default port is 22
75
+ # port 1234
76
+ ttl 1.day
77
+ path '/backups'
78
+ end
59
79
 
60
80
  # Store on a local filesystem path
61
81
  target :directory, '/media/backups/daily' do
@@ -79,7 +99,7 @@ Default configurations and commandline options
79
99
  If you place your backup configurations in the file `/etc/outback.conf` they
80
100
  will be read automatically when the outback executable is invoked. Make
81
101
  sure to have correct permissions on the configuration files, as they might
82
- include database passwords.
102
+ include database and encryption passwords.
83
103
 
84
104
  Alternatively, you can pass in the configuration file to read as a
85
105
  commandline argument. The default configuration file in /etc will then be
@@ -131,4 +151,10 @@ Other commandline options are:
131
151
  * `-t` `--test` test configuration, then exit
132
152
  * `-l` `--list` list available configurations, then exit
133
153
  * `-h` `--help` display help
134
- * `--version` display version
154
+ * `--version` display version
155
+
156
+ Known Issues
157
+ ------------
158
+
159
+ The s3 gem currently doesn't support V4 signatures, which are required for
160
+ s3 regions introduced after January 2014.
@@ -1,30 +1,19 @@
1
1
  module Outback
2
2
  class Archive
3
- NAME_PATTERN = /([A-Za-z0-9.\-]+)_(\d{14})_(\w+)/
3
+ NAME_PATTERN = /\A([A-Za-z0-9.\-]+)_(\d{14})_(\w+)/
4
4
 
5
- attr_reader :filename, :backup_name, :timestamp, :source_name, :parent
5
+ attr_reader :filename, :backup_name, :timestamp, :source_name, :size
6
6
 
7
- def initialize(filename, parent)
8
- @filename, @parent = Pathname.new(filename), parent
7
+ def initialize(filename)
8
+ @filename = Pathname.new(filename)
9
9
  unless match_data = @filename.basename.to_s.match(NAME_PATTERN)
10
10
  raise ArgumentError, 'invalid name'
11
11
  end
12
12
  @backup_name, @timestamp, @source_name = match_data.captures[0..2]
13
13
  end
14
14
 
15
- def size
16
- filename.size
15
+ def to_s
16
+ "#{filename}"
17
17
  end
18
-
19
- def open
20
- filename.open
21
- end
22
-
23
- def outdated?
24
- if timestamp && parent && parent.ttl
25
- Time.now - Time.parse(timestamp) > parent.ttl
26
- end
27
- end
28
-
29
18
  end
30
19
  end
@@ -1,44 +1,62 @@
1
1
  module Outback
2
2
  class Backup
3
- attr_reader :configuration, :name, :timestamp, :archives, :tmpdir
4
- delegate :sources, :targets, :to => :configuration
3
+ include Logging
5
4
 
5
+ attr_reader :configuration, :name, :timestamp, :tmpdir
6
+
6
7
  def initialize(configuration)
7
8
  raise ArgumentError, "configuration required" unless configuration.is_a?(Outback::Configuration)
8
9
  @configuration = configuration
9
10
  @name = configuration.name
10
- @timestamp = Time.now.to_formatted_s(:number)
11
+ @timestamp = Time.now.strftime(Outback::TIME_FORMAT)
11
12
  end
12
13
 
13
14
  def run!
14
- @archives = []
15
- begin
16
- Outback.info "Using working directory #{configuration.tmpdir}" if configuration.tmpdir
17
- @tmpdir = Dir.mktmpdir([name, timestamp], configuration.tmpdir)
18
- @archives = create_archives
19
- Outback.info "Created #{@archives.size} archives"
20
- store_archives
21
- ensure
22
- FileUtils.remove_entry_secure(tmpdir)
23
- end
15
+ logger.info "Using working directory #{configuration.tmpdir}" if configuration.tmpdir
16
+ @tmpdir = Dir.mktmpdir([name, timestamp], configuration.tmpdir)
17
+ archives = create_archives
18
+ logger.info "Created #{archives.size} archives"
19
+ archives = process_archives(archives)
20
+ logger.info "Processed #{archives.size} archives"
21
+ store_archives(archives)
24
22
  purge_targets
23
+ ensure
24
+ FileUtils.remove_entry_secure(tmpdir)
25
+ end
26
+
27
+ [:sources, :processors, :targets].each do |collection|
28
+ define_method(collection) { configuration.public_send(collection) }
25
29
  end
26
30
 
27
31
  private
28
32
 
29
33
  def create_archives
30
- archives = sources.collect do |source|
31
- source.create_archives(name, timestamp, tmpdir)
32
- end
33
- archives.flatten.compact
34
+ sources.map { |source| source.create_archives(timestamp, tmpdir) }.flatten.compact
34
35
  end
35
36
 
36
- def store_archives
37
- targets.each { |target| target.put(archives) }
37
+ def process_archives(archives)
38
+ processors.inject(archives) { |archives, processor| processor.process!(archives) }.flatten.compact
39
+ end
40
+
41
+ def store_archives(archives)
42
+ targets.each do |target|
43
+ begin
44
+ target.put(archives)
45
+ rescue => e
46
+ logger.info "Error while storing to #{target}: #{e.inspect}"
47
+ 0
48
+ end
49
+ end
38
50
  end
39
51
 
40
52
  def purge_targets
41
- targets.each { |target| target.purge!(name) }
53
+ targets.each do |target|
54
+ begin
55
+ target.purge!
56
+ rescue => e
57
+ logger.info "Error while purging #{target}: #{e.inspect}"
58
+ end
59
+ end
42
60
  end
43
61
 
44
62
  end
data/lib/outback/cli.rb CHANGED
@@ -35,7 +35,11 @@ module Outback
35
35
  end
36
36
  option_parser.parse!
37
37
  Outback::Configuration.reset
38
- config_file = ARGV.first || DEFAULT_CONFIGURATION_FILE
38
+ config_file = if ARGV.first
39
+ ARGV.first.start_with?('/') ? ARGV.first : File.join(Dir.pwd, ARGV.first)
40
+ else
41
+ DEFAULT_CONFIGURATION_FILE
42
+ end
39
43
  begin
40
44
  load config_file
41
45
  rescue ConfigurationError => conf_error
@@ -83,4 +87,4 @@ module Outback
83
87
  end
84
88
 
85
89
  end
86
- end
90
+ end
@@ -1,10 +1,11 @@
1
1
  module Outback
2
2
  class Configuration
3
+
3
4
  @loaded = []
4
5
 
5
6
  class << self
6
7
  def add(configuration)
7
- raise ConfigurationError.new("duplicate configuration #{configuration.name}") if loaded.any?(&its.name == configuration.name)
8
+ raise ConfigurationError, "duplicate configuration #{configuration.name}" if loaded.any?(&its.name == configuration.name)
8
9
  loaded << configuration
9
10
  end
10
11
 
@@ -21,13 +22,13 @@ module Outback
21
22
  end
22
23
  end
23
24
 
24
- attr_reader :name, :sources, :targets, :errors
25
+ attr_reader :name, :sources, :targets, :processors, :errors
25
26
 
26
27
  def initialize(name, &block)
27
- raise(ConfigurationError.new("configuration name can't be blank")) if name.blank?
28
- @name = name.to_s
29
- raise(ConfigurationError.new('configuration name may not contain underscores')) if @name.include?('_')
30
- @sources, @targets, @errors = [], [], []
28
+ name = name.to_s
29
+ raise ConfigurationError, "Illegal configuration name #{name.inspect}" unless name.match(/\A[a-z][a-z0-9_\-.]+\z/)
30
+ @name = name
31
+ @sources, @processors, @targets, @errors = [], [], [], []
31
32
  if block_given?
32
33
  if block.arity == 1 then yield(self) else instance_eval(&block) end
33
34
  end
@@ -48,21 +49,25 @@ module Outback
48
49
  @tmpdir
49
50
  elsif args.size == 1
50
51
  dir = Pathname.new(args.first).realpath
51
- raise(ConfigurationError.new("tmpdir #{dir} is no directory")) unless dir.directory?
52
+ raise ConfigurationError, "tmpdir #{dir} is not a directory" unless dir.directory?
52
53
  @tmpdir = dir
53
54
  else
54
- raise ConfigurationError.new("tmpdir: wrong number of arguments(#{args.size} for 1)")
55
+ raise ConfigurationError, "tmpdir: wrong number of arguments(#{args.size} for 1)"
55
56
  end
56
57
  end
57
58
 
58
59
  protected
59
60
 
60
61
  def source(type, *args, &block)
61
- "Outback::#{type.to_s.classify}Source".constantize.configure(*args, &block).tap { |instance| sources << instance }
62
+ "Outback::#{type.to_s.classify}Source".constantize.configure(name, *args, &block).tap { |instance| sources << instance }
63
+ end
64
+
65
+ def processor(type, *args, &block)
66
+ "Outback::#{type.to_s.classify}Processor".constantize.configure(name, *args, &block).tap { |instance| processors << instance }
62
67
  end
63
68
 
64
69
  def target(type, *args, &block)
65
- "Outback::#{type.to_s.classify}Target".constantize.configure(*args, &block).tap { |instance| targets << instance }
70
+ "Outback::#{type.to_s.classify}Target".constantize.configure(name, *args, &block).tap { |instance| targets << instance }
66
71
  end
67
72
 
68
73
  def error(message)
@@ -2,7 +2,8 @@ module Outback
2
2
  class DirectorySource < Source
3
3
  attr_reader :path
4
4
 
5
- def initialize(path)
5
+ def initialize(backup_name, path)
6
+ super(backup_name)
6
7
  @path = path
7
8
  end
8
9
 
@@ -18,18 +19,18 @@ module Outback
18
19
  excludes.concat(paths.map(&:to_s)).uniq!
19
20
  end
20
21
 
21
- def create_archives(backup_name, timestamp, tmpdir)
22
+ def create_archives(timestamp, tmpdir)
22
23
  source_dir = Pathname.new(path).realpath
23
24
  archive_name = Pathname.new(tmpdir).join("#{backup_name}_#{timestamp}_#{source_name}.tar.gz")
24
25
  exclude_list = Pathname.new(tmpdir).join('exclude_list.txt')
25
26
  File.open(exclude_list, 'w') { |f| f << excludes.map { |e| e.to_s.sub(/\A\//, '') }.join("\n") }
26
27
  verbose_switch = Outback.verbose? ? 'v' : ''
27
28
  commandline = "tar -cz#{verbose_switch}pf #{archive_name} --exclude-from=#{exclude_list} --directory=/ #{source_dir.to_s.sub(/\A\//, '')}"
28
- Outback.debug "executing command: #{commandline}"
29
+ logger.debug "executing command: #{commandline}"
29
30
  result = `#{commandline}`
30
- Outback.debug "result: #{result}"
31
- Outback.info "Archived directory #{path}"
32
- [TempArchive.new(archive_name, self)]
31
+ logger.debug "result: #{result}"
32
+ logger.info "Archived directory #{path}"
33
+ [SourceArchive.new(archive_name)]
33
34
  end
34
35
  end
35
- end
36
+ end
@@ -3,7 +3,8 @@ module Outback
3
3
  attr_reader :path
4
4
  attr_setter :user, :group, :directory_permissions, :archive_permissions, :ttl, :move
5
5
 
6
- def initialize(path)
6
+ def initialize(backup_name, path)
7
+ super(backup_name)
7
8
  @path = Pathname.new(path)
8
9
  end
9
10
 
@@ -11,39 +12,45 @@ module Outback
11
12
  (user and group) or (not user and not group)
12
13
  end
13
14
 
14
- def display_name
15
+ def to_s
15
16
  "directory:#{path}"
16
17
  end
17
18
 
18
19
  def put(archives)
19
- Dir.mkdir(path) unless path.directory?
20
+ FileUtils.mkdir_p(path) unless path.directory?
20
21
  FileUtils.chmod directory_permissions || 0700, path
21
22
  size = 0
22
23
  archives.each do |archive|
23
24
  basename = Pathname.new(archive.filename).basename
24
25
  if move
25
- Outback.debug "moving #{archive.filename} to #{path}"
26
+ logger.debug "moving #{archive.filename} to #{path}"
26
27
  FileUtils.mv archive.filename, path
27
28
  else
28
- Outback.debug "copying #{archive.filename} to #{path}"
29
+ logger.debug "copying #{archive.filename} to #{path}"
29
30
  FileUtils.cp_r archive.filename, path
30
31
  end
31
32
  archived_file = path.join(basename)
32
- Outback.debug "setting permissions for #{archived_file}"
33
+ logger.debug "setting permissions for #{archived_file}"
33
34
  FileUtils.chmod archive_permissions || 0600, archived_file
34
35
  if user && group
35
- Outback.debug "setting owner #{user}, group #{group} for #{archived_file}"
36
+ logger.debug "setting owner #{user}, group #{group} for #{archived_file}"
36
37
  FileUtils.chown user, group, archived_file
37
38
  end
38
39
  size += archived_file.size
39
40
  end
40
- Outback.info "#{move ? 'Moved' : 'Copied'} #{archives.size} archives (#{size} bytes) to #{display_name}"
41
+ logger.info "#{move ? 'Moved' : 'Copied'} #{archives.size} archives (#{size} bytes) to #{self}"
41
42
  archives.size
42
43
  end
44
+
45
+ private
43
46
 
44
- def list_archives(name)
45
- path.files(Archive::NAME_PATTERN).map { |f| DirectoryArchive.new(f, self) }.select(&its.backup_name == name)
47
+ def list_all_archives
48
+ path.files(Archive::NAME_PATTERN).map { |f| build_archive(f.to_s, f.size) }
49
+ end
50
+
51
+ def unlink_archive!(archive)
52
+ archive.filename.unlink
46
53
  end
47
54
 
48
55
  end
49
- end
56
+ end
@@ -0,0 +1,34 @@
1
+ require 'openssl'
2
+ require 'open3'
3
+
4
+ module Outback
5
+ class EncryptionProcessor < Processor
6
+
7
+ attr_setter :password, :cipher
8
+
9
+ def cipher
10
+ @cipher ||= 'aes-256-cbc'
11
+ end
12
+
13
+ def to_s
14
+ "encryption:#{cipher}"
15
+ end
16
+
17
+ private
18
+
19
+ def process_archive!(archive)
20
+ result = nil
21
+ outfile = Pathname.new("#{archive.filename}.enc")
22
+ logger.debug "Encrypting #{archive} with #{self}"
23
+ Open3.popen3("openssl enc -#{cipher} -pass stdin -in #{archive.filename} -out #{outfile}") do |stdin, stdout, stderr, wait_thr|
24
+ stdin << password
25
+ stdin.close
26
+ result = wait_thr.value
27
+ end
28
+ raise ProcessingError, "error processing archive #{archive} in #{self}" unless result.success?
29
+ raise ProcessingError, "outfile #{outfile} not found" unless outfile.file?
30
+ archive.unlink
31
+ SourceArchive.new(outfile)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module Outback
2
+ class Error < StandardError; end
3
+
4
+ class ConfigurationError < Error; end
5
+ class BackupError < Error; end
6
+ class ProcessingError < Error; end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Outback
2
+ module Logging
3
+ def logger
4
+ Outback
5
+ end
6
+ end
7
+ end
@@ -7,7 +7,7 @@ module Outback
7
7
  end
8
8
 
9
9
  def excludes
10
- @excludes ||= []
10
+ @excludes ||= ['mysql', 'performance_schema', 'information_schema']
11
11
  end
12
12
 
13
13
  def database(*names)
@@ -22,13 +22,13 @@ module Outback
22
22
  user && password
23
23
  end
24
24
 
25
- def create_archives(backup_name, timestamp, tmpdir)
25
+ def create_archives(timestamp, tmpdir)
26
26
  mysql_host = host || 'localhost'
27
27
  mysql_port = (port || 3306) unless socket
28
28
  if databases.empty?
29
- # (host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil)
29
+ # (host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil)
30
30
  mysql = Mysql.connect(mysql_host, user, password, nil, mysql_port, socket)
31
- @databases = mysql.databases - excludes
31
+ @databases = mysql.list_dbs - excludes
32
32
  mysql.close
33
33
  end
34
34
 
@@ -37,13 +37,13 @@ module Outback
37
37
  mysql_conf_file = Pathname.new(tmpdir).join('outback_my.cnf')
38
38
  File.open(mysql_conf_file, 'w') { |f| f << "[client]\npassword=#{password}\n" }
39
39
  FileUtils.chmod 0600, mysql_conf_file
40
- Outback.debug "MysqlSource: dumping database '#{database}'"
40
+ logger.debug "MysqlSource: dumping database '#{database}'"
41
41
  commandline = "mysqldump --defaults-extra-file=#{mysql_conf_file} --opt --user=#{user} --host=#{mysql_host} --port=#{mysql_port} #{database} | gzip > #{archive_name}"
42
42
  result = `#{commandline}`.strip
43
- Outback.debug(result) unless result.blank?
44
- Outback.info "Archived database #{database}"
45
- TempArchive.new(archive_name, self).tap { |archive| Outback.debug "dumped #{archive.filename.basename} with #{archive.size} bytes" }
43
+ logger.debug(result) unless result.blank?
44
+ logger.info "Archived database #{database}"
45
+ SourceArchive.new(archive_name).tap { |archive| logger.debug "dumped #{archive.filename.basename} with #{archive.size} bytes" }
46
46
  end
47
47
  end
48
48
  end
49
- end
49
+ end
@@ -0,0 +1,17 @@
1
+ module Outback
2
+ class Processor
3
+ include Configurable
4
+ include Logging
5
+
6
+ attr_reader :backup_name
7
+
8
+ def initialize(backup_name)
9
+ @backup_name = backup_name
10
+ end
11
+
12
+ def process!(archives)
13
+ archives.map { |archive| process_archive!(archive) }.flatten.compact
14
+ end
15
+
16
+ end
17
+ end
@@ -2,7 +2,7 @@ module Outback
2
2
  class S3Target < Target
3
3
  attr_setter :bucket_name, :access_key, :secret_key, :ttl, :prefix
4
4
 
5
- def display_name
5
+ def to_s
6
6
  "s3:#{bucket_name}/#{prefix}"
7
7
  end
8
8
 
@@ -12,7 +12,7 @@ module Outback
12
12
 
13
13
  def service(force_reconnect = false)
14
14
  @service = nil if force_reconnect
15
- @service ||= S3::Service.new(:access_key_id => access_key, :secret_access_key => secret_key)
15
+ @service ||= S3::Service.new(access_key_id: access_key, secret_access_key: secret_key, use_ssl: true)
16
16
  end
17
17
 
18
18
  def bucket
@@ -23,7 +23,7 @@ module Outback
23
23
  size = count = 0
24
24
  archives.each do |archive|
25
25
  object_name = [prefix.to_s, archive.filename.basename.to_s].join('/')
26
- Outback.debug "S3Target: storing #{archive.filename} in s3://#{bucket_name}/#{object_name}"
26
+ logger.debug "S3Target: storing #{archive.filename} in s3://#{bucket_name}/#{object_name}"
27
27
  object = bucket.objects.build(object_name)
28
28
  object.content = archive.open
29
29
  object.acl = :private
@@ -32,17 +32,26 @@ module Outback
32
32
  size += archive.size
33
33
  count += 1
34
34
  else
35
- Outback.error "S3 archive upload failed: #{object_name}"
35
+ logger.error "S3 archive upload failed: #{object_name}"
36
36
  end
37
37
  end
38
- Outback.info "Uploaded #{count} archives (#{size} bytes) to #{display_name}"
38
+ logger.info "Uploaded #{count} archives (#{size} bytes) to #{self}"
39
39
  count
40
40
  end
41
41
 
42
- def list_archives(name)
42
+ private
43
+
44
+ def list_all_archives
43
45
  entries = bucket.objects.select { |e| e.key.start_with?(prefix.to_s) && e.key[prefix.to_s.size..-1].match(Archive::NAME_PATTERN) }
44
- entries.map { |e| S3Archive.new(e.key, self) }.select(&its.backup_name == name)
46
+ entries.map { |e| build_archive(e.key, e.size) }
45
47
  end
48
+
49
+ def unlink_archive!(archive)
50
+ logger.debug "purging S3Archive: #{archive.filename}"
51
+ object = bucket.objects.find(archive.filename.to_s)
52
+ return true if object && object.destroy
53
+ raise BackupError, "could not find object #{archive.filename} for purging"
54
+ end
55
+
46
56
  end
47
-
48
- end
57
+ end
@@ -0,0 +1,70 @@
1
+ require 'net/sftp'
2
+
3
+ module Outback
4
+ class SftpTarget < Target
5
+
6
+ attr_reader :host
7
+ attr_setter :user, :password, :port, :path, :ttl, :ssh_opts
8
+
9
+ def initialize(backup_name, host)
10
+ super(backup_name)
11
+ @host = host
12
+ end
13
+
14
+ def valid?
15
+ host.present?
16
+ end
17
+
18
+ def to_s
19
+ "sftp:#{user}@#{host}#{':' + port.to_s if port}:#{path}"
20
+ end
21
+
22
+ def put(archives)
23
+ size = count = 0
24
+ connect do
25
+ archives.each do |archive|
26
+ basename = archive.filename.basename.to_s
27
+ upload_filename = path ? File.join(path, basename) : basename
28
+ logger.debug "SftpTarget: storing #{archive.filename} in sftp://#{user}@#{host}#{':' + port.to_s if port}:#{upload_filename}"
29
+ connection.upload!(archive.filename.to_s, upload_filename)
30
+ size += archive.size
31
+ count += 1
32
+ end
33
+ end
34
+ logger.info "Uploaded #{count} archives (#{size} bytes) to #{self}"
35
+ count
36
+ end
37
+
38
+ private
39
+
40
+ def connect
41
+ result = nil
42
+ Net::SFTP.start(host, user, ssh_options) do |sftp|
43
+ @connection = sftp
44
+ result = yield
45
+ end
46
+ result
47
+ ensure
48
+ @connection = nil
49
+ end
50
+
51
+ def ssh_options
52
+ options = ssh_opts || {}
53
+ if password
54
+ options[:password] = password
55
+ options[:auth_methods] ||= %w[password]
56
+ end
57
+ options[:port] = port if port
58
+ options
59
+ end
60
+
61
+ def list_all_archives
62
+ connection.dir.entries(path).select(&:file?).map { |entry| build_archive(File.join(path, entry.name), entry.attributes.size.to_i) }
63
+ end
64
+
65
+ def unlink_archive!(archive)
66
+ connection.remove!(archive.filename)
67
+ end
68
+
69
+ end
70
+ end