ext_backup 5.0.0.beta.2.1

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 (137) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +33 -0
  4. data/bin/backup +5 -0
  5. data/bin/docker_test +24 -0
  6. data/lib/backup.rb +140 -0
  7. data/lib/backup/archive.rb +169 -0
  8. data/lib/backup/binder.rb +18 -0
  9. data/lib/backup/cleaner.rb +112 -0
  10. data/lib/backup/cli.rb +370 -0
  11. data/lib/backup/cloud_io/base.rb +38 -0
  12. data/lib/backup/cloud_io/cloud_files.rb +296 -0
  13. data/lib/backup/cloud_io/s3.rb +253 -0
  14. data/lib/backup/compressor/base.rb +32 -0
  15. data/lib/backup/compressor/bzip2.rb +35 -0
  16. data/lib/backup/compressor/custom.rb +49 -0
  17. data/lib/backup/compressor/gzip.rb +73 -0
  18. data/lib/backup/config.rb +128 -0
  19. data/lib/backup/config/dsl.rb +102 -0
  20. data/lib/backup/config/helpers.rb +137 -0
  21. data/lib/backup/database/base.rb +86 -0
  22. data/lib/backup/database/mongodb.rb +186 -0
  23. data/lib/backup/database/mysql.rb +191 -0
  24. data/lib/backup/database/openldap.rb +93 -0
  25. data/lib/backup/database/postgresql.rb +132 -0
  26. data/lib/backup/database/redis.rb +176 -0
  27. data/lib/backup/database/riak.rb +79 -0
  28. data/lib/backup/database/sqlite.rb +55 -0
  29. data/lib/backup/encryptor/base.rb +27 -0
  30. data/lib/backup/encryptor/gpg.rb +737 -0
  31. data/lib/backup/encryptor/open_ssl.rb +74 -0
  32. data/lib/backup/errors.rb +53 -0
  33. data/lib/backup/logger.rb +197 -0
  34. data/lib/backup/logger/console.rb +48 -0
  35. data/lib/backup/logger/fog_adapter.rb +25 -0
  36. data/lib/backup/logger/logfile.rb +131 -0
  37. data/lib/backup/logger/syslog.rb +114 -0
  38. data/lib/backup/model.rb +472 -0
  39. data/lib/backup/notifier/base.rb +126 -0
  40. data/lib/backup/notifier/campfire.rb +61 -0
  41. data/lib/backup/notifier/command.rb +99 -0
  42. data/lib/backup/notifier/datadog.rb +104 -0
  43. data/lib/backup/notifier/flowdock.rb +99 -0
  44. data/lib/backup/notifier/hipchat.rb +116 -0
  45. data/lib/backup/notifier/http_post.rb +114 -0
  46. data/lib/backup/notifier/mail.rb +232 -0
  47. data/lib/backup/notifier/nagios.rb +65 -0
  48. data/lib/backup/notifier/pagerduty.rb +79 -0
  49. data/lib/backup/notifier/prowl.rb +68 -0
  50. data/lib/backup/notifier/pushover.rb +71 -0
  51. data/lib/backup/notifier/ses.rb +123 -0
  52. data/lib/backup/notifier/slack.rb +147 -0
  53. data/lib/backup/notifier/twitter.rb +55 -0
  54. data/lib/backup/notifier/zabbix.rb +60 -0
  55. data/lib/backup/package.rb +51 -0
  56. data/lib/backup/packager.rb +106 -0
  57. data/lib/backup/pipeline.rb +120 -0
  58. data/lib/backup/splitter.rb +73 -0
  59. data/lib/backup/storage/base.rb +66 -0
  60. data/lib/backup/storage/cloud_files.rb +156 -0
  61. data/lib/backup/storage/cycler.rb +70 -0
  62. data/lib/backup/storage/dropbox.rb +206 -0
  63. data/lib/backup/storage/ftp.rb +116 -0
  64. data/lib/backup/storage/local.rb +61 -0
  65. data/lib/backup/storage/qiniu.rb +65 -0
  66. data/lib/backup/storage/rsync.rb +246 -0
  67. data/lib/backup/storage/s3.rb +155 -0
  68. data/lib/backup/storage/scp.rb +65 -0
  69. data/lib/backup/storage/sftp.rb +80 -0
  70. data/lib/backup/syncer/base.rb +67 -0
  71. data/lib/backup/syncer/cloud/base.rb +176 -0
  72. data/lib/backup/syncer/cloud/cloud_files.rb +81 -0
  73. data/lib/backup/syncer/cloud/local_file.rb +97 -0
  74. data/lib/backup/syncer/cloud/s3.rb +109 -0
  75. data/lib/backup/syncer/rsync/base.rb +50 -0
  76. data/lib/backup/syncer/rsync/local.rb +27 -0
  77. data/lib/backup/syncer/rsync/pull.rb +47 -0
  78. data/lib/backup/syncer/rsync/push.rb +201 -0
  79. data/lib/backup/template.rb +41 -0
  80. data/lib/backup/utilities.rb +233 -0
  81. data/lib/backup/version.rb +3 -0
  82. data/lib/ext_backup.rb +5 -0
  83. data/lib/ext_backup/version.rb +5 -0
  84. data/templates/cli/archive +28 -0
  85. data/templates/cli/compressor/bzip2 +4 -0
  86. data/templates/cli/compressor/custom +7 -0
  87. data/templates/cli/compressor/gzip +4 -0
  88. data/templates/cli/config +123 -0
  89. data/templates/cli/databases/mongodb +15 -0
  90. data/templates/cli/databases/mysql +18 -0
  91. data/templates/cli/databases/openldap +24 -0
  92. data/templates/cli/databases/postgresql +16 -0
  93. data/templates/cli/databases/redis +16 -0
  94. data/templates/cli/databases/riak +17 -0
  95. data/templates/cli/databases/sqlite +11 -0
  96. data/templates/cli/encryptor/gpg +27 -0
  97. data/templates/cli/encryptor/openssl +9 -0
  98. data/templates/cli/model +26 -0
  99. data/templates/cli/notifier/zabbix +15 -0
  100. data/templates/cli/notifiers/campfire +12 -0
  101. data/templates/cli/notifiers/command +32 -0
  102. data/templates/cli/notifiers/datadog +57 -0
  103. data/templates/cli/notifiers/flowdock +16 -0
  104. data/templates/cli/notifiers/hipchat +16 -0
  105. data/templates/cli/notifiers/http_post +32 -0
  106. data/templates/cli/notifiers/mail +24 -0
  107. data/templates/cli/notifiers/nagios +13 -0
  108. data/templates/cli/notifiers/pagerduty +12 -0
  109. data/templates/cli/notifiers/prowl +11 -0
  110. data/templates/cli/notifiers/pushover +11 -0
  111. data/templates/cli/notifiers/ses +15 -0
  112. data/templates/cli/notifiers/slack +22 -0
  113. data/templates/cli/notifiers/twitter +13 -0
  114. data/templates/cli/splitter +7 -0
  115. data/templates/cli/storages/cloud_files +11 -0
  116. data/templates/cli/storages/dropbox +20 -0
  117. data/templates/cli/storages/ftp +13 -0
  118. data/templates/cli/storages/local +8 -0
  119. data/templates/cli/storages/qiniu +12 -0
  120. data/templates/cli/storages/rsync +17 -0
  121. data/templates/cli/storages/s3 +16 -0
  122. data/templates/cli/storages/scp +15 -0
  123. data/templates/cli/storages/sftp +15 -0
  124. data/templates/cli/syncers/cloud_files +22 -0
  125. data/templates/cli/syncers/rsync_local +20 -0
  126. data/templates/cli/syncers/rsync_pull +28 -0
  127. data/templates/cli/syncers/rsync_push +28 -0
  128. data/templates/cli/syncers/s3 +27 -0
  129. data/templates/general/links +3 -0
  130. data/templates/general/version.erb +2 -0
  131. data/templates/notifier/mail/failure.erb +16 -0
  132. data/templates/notifier/mail/success.erb +16 -0
  133. data/templates/notifier/mail/warning.erb +16 -0
  134. data/templates/storage/dropbox/authorization_url.erb +6 -0
  135. data/templates/storage/dropbox/authorized.erb +4 -0
  136. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  137. metadata +506 -0
@@ -0,0 +1,155 @@
1
+ require "backup/cloud_io/s3"
2
+
3
+ module Backup
4
+ module Storage
5
+ class S3 < Base
6
+ include Storage::Cycler
7
+ class Error < Backup::Error; end
8
+
9
+ ##
10
+ # Amazon Simple Storage Service (S3) Credentials
11
+ attr_accessor :access_key_id, :secret_access_key, :use_iam_profile
12
+
13
+ ##
14
+ # Amazon S3 bucket name
15
+ attr_accessor :bucket
16
+
17
+ ##
18
+ # Region of the specified S3 bucket
19
+ attr_accessor :region
20
+
21
+ ##
22
+ # Multipart chunk size, specified in MiB.
23
+ #
24
+ # Each package file larger than +chunk_size+
25
+ # will be uploaded using S3 Multipart Upload.
26
+ #
27
+ # Minimum: 5 (but may be disabled with 0)
28
+ # Maximum: 5120
29
+ # Default: 5
30
+ attr_accessor :chunk_size
31
+
32
+ ##
33
+ # Number of times to retry failed operations.
34
+ #
35
+ # Default: 10
36
+ attr_accessor :max_retries
37
+
38
+ ##
39
+ # Time in seconds to pause before each retry.
40
+ #
41
+ # Default: 30
42
+ attr_accessor :retry_waitsec
43
+
44
+ ##
45
+ # Encryption algorithm to use for Amazon Server-Side Encryption
46
+ #
47
+ # Supported values:
48
+ #
49
+ # - :aes256
50
+ #
51
+ # Default: nil
52
+ attr_accessor :encryption
53
+
54
+ ##
55
+ # Storage class to use for the S3 objects uploaded
56
+ #
57
+ # Supported values:
58
+ #
59
+ # - :standard (default)
60
+ # - :standard_ia
61
+ # - :reduced_redundancy
62
+ #
63
+ # Default: :standard
64
+ attr_accessor :storage_class
65
+
66
+ ##
67
+ # Additional options to pass along to fog.
68
+ # e.g. Fog::Storage.new({ :provider => 'AWS' }.merge(fog_options))
69
+ attr_accessor :fog_options
70
+
71
+ def initialize(model, storage_id = nil)
72
+ super
73
+
74
+ @chunk_size ||= 5 # MiB
75
+ @max_retries ||= 10
76
+ @retry_waitsec ||= 30
77
+ @path ||= "backups"
78
+ @storage_class ||= :standard
79
+
80
+ @path = @path.sub(/^\//, "")
81
+
82
+ check_configuration
83
+ end
84
+
85
+ private
86
+
87
+ def cloud_io
88
+ @cloud_io ||= CloudIO::S3.new(
89
+ access_key_id: access_key_id,
90
+ secret_access_key: secret_access_key,
91
+ use_iam_profile: use_iam_profile,
92
+ region: region,
93
+ bucket: bucket,
94
+ encryption: encryption,
95
+ storage_class: storage_class,
96
+ max_retries: max_retries,
97
+ retry_waitsec: retry_waitsec,
98
+ chunk_size: chunk_size,
99
+ fog_options: fog_options
100
+ )
101
+ end
102
+
103
+ def transfer!
104
+ package.filenames.each do |filename|
105
+ src = File.join(Config.tmp_path, filename)
106
+ dest = File.join(remote_path, filename)
107
+ Logger.info "Storing '#{bucket}/#{dest}'..."
108
+ cloud_io.upload(src, dest)
109
+ end
110
+ end
111
+
112
+ # Called by the Cycler.
113
+ # Any error raised will be logged as a warning.
114
+ def remove!(package)
115
+ Logger.info "Removing backup package dated #{package.time}..."
116
+
117
+ remote_path = remote_path_for(package)
118
+ objects = cloud_io.objects(remote_path)
119
+
120
+ raise Error, "Package at '#{remote_path}' not found" if objects.empty?
121
+
122
+ cloud_io.delete(objects)
123
+ end
124
+
125
+ def check_configuration
126
+ required =
127
+ if use_iam_profile
128
+ %w[bucket]
129
+ else
130
+ %w[access_key_id secret_access_key bucket]
131
+ end
132
+ raise Error, <<-EOS if required.map { |name| send(name) }.any?(&:nil?)
133
+ Configuration Error
134
+ #{required.map { |name| "##{name}" }.join(", ")} are all required
135
+ EOS
136
+
137
+ raise Error, <<-EOS if chunk_size > 0 && !chunk_size.between?(5, 5120)
138
+ Configuration Error
139
+ #chunk_size must be between 5 and 5120 (or 0 to disable multipart)
140
+ EOS
141
+
142
+ raise Error, <<-EOS if encryption && encryption.to_s.upcase != "AES256"
143
+ Configuration Error
144
+ #encryption must be :aes256 or nil
145
+ EOS
146
+
147
+ classes = ["STANDARD", "STANDARD_IA", "REDUCED_REDUNDANCY"]
148
+ raise Error, <<-EOS unless classes.include?(storage_class.to_s.upcase)
149
+ Configuration Error
150
+ #storage_class must be :standard or :standard_ia or :reduced_redundancy
151
+ EOS
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,65 @@
1
+ require "net/scp"
2
+
3
+ module Backup
4
+ module Storage
5
+ class SCP < Base
6
+ include Storage::Cycler
7
+ class Error < Backup::Error; end
8
+
9
+ ##
10
+ # Server credentials
11
+ attr_accessor :username, :password, :ssh_options
12
+
13
+ ##
14
+ # Server IP Address and SCP port
15
+ attr_accessor :ip, :port
16
+
17
+ def initialize(model, storage_id = nil)
18
+ super
19
+
20
+ @port ||= 22
21
+ @path ||= "backups"
22
+ @ssh_options ||= {}
23
+ path.sub!(/^~\//, "")
24
+ end
25
+
26
+ private
27
+
28
+ def connection
29
+ Net::SSH.start(
30
+ ip, username, { password: password, port: port }.merge(ssh_options)
31
+ ) { |ssh| yield ssh }
32
+ end
33
+
34
+ def transfer!
35
+ connection do |ssh|
36
+ ssh.exec!("mkdir -p '#{remote_path}'")
37
+
38
+ package.filenames.each do |filename|
39
+ src = File.join(Config.tmp_path, filename)
40
+ dest = File.join(remote_path, filename)
41
+ Logger.info "Storing '#{ip}:#{dest}'..."
42
+ ssh.scp.upload!(src, dest)
43
+ end
44
+ end
45
+ end
46
+
47
+ # Called by the Cycler.
48
+ # Any error raised will be logged as a warning.
49
+ def remove!(package)
50
+ Logger.info "Removing backup package dated #{package.time}..."
51
+
52
+ errors = []
53
+ connection do |ssh|
54
+ ssh.exec!("rm -r '#{remote_path_for(package)}'") do |_, stream, data|
55
+ errors << data if stream == :stderr
56
+ end
57
+ end
58
+ unless errors.empty?
59
+ raise Error, "Net::SSH reported the following errors:\n" +
60
+ errors.join("\n")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,80 @@
1
+ require "net/sftp"
2
+
3
+ module Backup
4
+ module Storage
5
+ class SFTP < Base
6
+ include Storage::Cycler
7
+
8
+ ##
9
+ # Server credentials
10
+ attr_accessor :username, :password, :ssh_options
11
+
12
+ ##
13
+ # Server IP Address and SFTP port
14
+ attr_accessor :ip, :port
15
+
16
+ def initialize(model, storage_id = nil)
17
+ super
18
+
19
+ @ssh_options ||= {}
20
+ @port ||= 22
21
+ @path ||= "backups"
22
+ path.sub!(/^~\//, "")
23
+ end
24
+
25
+ private
26
+
27
+ def connection
28
+ Net::SFTP.start(
29
+ ip, username, { password: password, port: port }.merge(ssh_options)
30
+ ) { |sftp| yield sftp }
31
+ end
32
+
33
+ def transfer!
34
+ connection do |sftp|
35
+ create_remote_path(sftp)
36
+
37
+ package.filenames.each do |filename|
38
+ src = File.join(Config.tmp_path, filename)
39
+ dest = File.join(remote_path, filename)
40
+ Logger.info "Storing '#{ip}:#{dest}'..."
41
+ sftp.upload!(src, dest)
42
+ end
43
+ end
44
+ end
45
+
46
+ # Called by the Cycler.
47
+ # Any error raised will be logged as a warning.
48
+ def remove!(package)
49
+ Logger.info "Removing backup package dated #{package.time}..."
50
+
51
+ remote_path = remote_path_for(package)
52
+ connection do |sftp|
53
+ package.filenames.each do |filename|
54
+ sftp.remove!(File.join(remote_path, filename))
55
+ end
56
+
57
+ sftp.rmdir!(remote_path)
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Creates (if they don't exist yet) all the directories on the remote
63
+ # server in order to upload the backup file. Net::SFTP does not support
64
+ # paths to directories that don't yet exist when creating new
65
+ # directories. Instead, we split the parts up in to an array (for each
66
+ # '/') and loop through that to create the directories one by one.
67
+ # Net::SFTP raises an exception when the directory it's trying to create
68
+ # already exists, so we have rescue it
69
+ def create_remote_path(sftp)
70
+ path_parts = []
71
+ remote_path.split("/").each do |path_part|
72
+ path_parts << path_part
73
+ begin
74
+ sftp.mkdir!(path_parts.join("/"))
75
+ rescue Net::SFTP::StatusException; end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,67 @@
1
+ module Backup
2
+ module Syncer
3
+ class Base
4
+ include Utilities::Helpers
5
+ include Config::Helpers
6
+
7
+ ##
8
+ # Path to store the synced files/directories to
9
+ attr_accessor :path
10
+
11
+ ##
12
+ # Flag for mirroring the files/directories
13
+ attr_accessor :mirror
14
+
15
+ ##
16
+ # Optional user-defined identifier to differentiate multiple syncers
17
+ # defined within a single backup model. Currently this is only used
18
+ # in the log messages.
19
+ attr_reader :syncer_id
20
+
21
+ attr_reader :excludes
22
+
23
+ def initialize(syncer_id = nil)
24
+ @syncer_id = syncer_id
25
+
26
+ load_defaults!
27
+
28
+ @mirror ||= false
29
+ @directories ||= []
30
+ @excludes ||= []
31
+ end
32
+
33
+ ##
34
+ # Syntactical suger for the DSL for adding directories
35
+ def directories(&block)
36
+ return @directories unless block_given?
37
+ instance_eval(&block)
38
+ end
39
+
40
+ def add(path)
41
+ directories << path
42
+ end
43
+
44
+ # For Cloud Syncers, +pattern+ can be a string (with shell-style
45
+ # wildcards) or a regex.
46
+ # For RSync, each +pattern+ will be passed to rsync's --exclude option.
47
+ def exclude(pattern)
48
+ excludes << pattern
49
+ end
50
+
51
+ private
52
+
53
+ def syncer_name
54
+ @syncer_name ||= self.class.to_s.sub("Backup::", "") +
55
+ (syncer_id ? " (#{syncer_id})" : "")
56
+ end
57
+
58
+ def log!(action)
59
+ msg = case action
60
+ when :started then "Started..."
61
+ when :finished then "Finished!"
62
+ end
63
+ Logger.info "#{syncer_name} #{msg}"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,176 @@
1
+ module Backup
2
+ module Syncer
3
+ module Cloud
4
+ class Error < Backup::Error; end
5
+
6
+ class Base < Syncer::Base
7
+ MUTEX = Mutex.new
8
+
9
+ ##
10
+ # Number of threads to use for concurrency.
11
+ #
12
+ # Default: 0 (no concurrency)
13
+ attr_accessor :thread_count
14
+
15
+ ##
16
+ # Number of times to retry failed operations.
17
+ #
18
+ # Default: 10
19
+ attr_accessor :max_retries
20
+
21
+ ##
22
+ # Time in seconds to pause before each retry.
23
+ #
24
+ # Default: 30
25
+ attr_accessor :retry_waitsec
26
+
27
+ def initialize(syncer_id = nil, &block)
28
+ super
29
+ instance_eval(&block) if block_given?
30
+
31
+ @thread_count ||= 0
32
+ @max_retries ||= 10
33
+ @retry_waitsec ||= 30
34
+
35
+ @path ||= "backups"
36
+ @path = path.sub(/^\//, "")
37
+ end
38
+
39
+ def perform!
40
+ log!(:started)
41
+ @transfer_count = 0
42
+ @unchanged_count = 0
43
+ @skipped_count = 0
44
+ @orphans = thread_count > 0 ? Queue.new : []
45
+
46
+ directories.each { |dir| sync_directory(dir) }
47
+ orphans_result = process_orphans
48
+
49
+ Logger.info "\nSummary:"
50
+ Logger.info "\s\sTransferred Files: #{@transfer_count}"
51
+ Logger.info "\s\s#{orphans_result}"
52
+ Logger.info "\s\sUnchanged Files: #{@unchanged_count}"
53
+ Logger.warn "\s\sSkipped Files: #{@skipped_count}" if @skipped_count > 0
54
+ log!(:finished)
55
+ end
56
+
57
+ private
58
+
59
+ def sync_directory(dir)
60
+ remote_base = path.empty? ? File.basename(dir) :
61
+ File.join(path, File.basename(dir))
62
+ Logger.info "Gathering remote data for '#{remote_base}'..."
63
+ remote_files = get_remote_files(remote_base)
64
+
65
+ Logger.info("Gathering local data for '#{File.expand_path(dir)}'...")
66
+ local_files = LocalFile.find(dir, excludes)
67
+
68
+ relative_paths = (local_files.keys | remote_files.keys).sort
69
+ if relative_paths.empty?
70
+ Logger.info "No local or remote files found"
71
+ else
72
+ Logger.info "Syncing..."
73
+ sync_block = proc do |relative_path|
74
+ local_file = local_files[relative_path]
75
+ remote_md5 = remote_files[relative_path]
76
+ remote_path = File.join(remote_base, relative_path)
77
+ sync_file(local_file, remote_path, remote_md5)
78
+ end
79
+
80
+ if thread_count > 0
81
+ sync_in_threads(relative_paths, sync_block)
82
+ else
83
+ relative_paths.each(&sync_block)
84
+ end
85
+ end
86
+ end
87
+
88
+ def sync_in_threads(relative_paths, sync_block)
89
+ queue = Queue.new
90
+ queue << relative_paths.shift until relative_paths.empty?
91
+ num_threads = [thread_count, queue.size].min
92
+ Logger.info "\s\sUsing #{num_threads} Threads"
93
+ threads = Array.new(num_threads) do
94
+ Thread.new do
95
+ loop do
96
+ path = queue.shift(true) rescue nil
97
+ path ? sync_block.call(path) : break
98
+ end
99
+ end
100
+ end
101
+
102
+ # abort if any thread raises an exception
103
+ while threads.any?(&:alive?)
104
+ if threads.any? { |thr| thr.status.nil? }
105
+ threads.each(&:kill)
106
+ Thread.pass while threads.any?(&:alive?)
107
+ break
108
+ end
109
+ sleep num_threads * 0.1
110
+ end
111
+ threads.each(&:join)
112
+ end
113
+
114
+ # If an exception is raised in multiple threads, only the exception
115
+ # raised in the first thread that Thread#join is called on will be
116
+ # handled. So all exceptions are logged first with their details,
117
+ # then a generic exception is raised.
118
+ def sync_file(local_file, remote_path, remote_md5)
119
+ if local_file && File.exist?(local_file.path)
120
+ if local_file.md5 == remote_md5
121
+ MUTEX.synchronize { @unchanged_count += 1 }
122
+ else
123
+ Logger.info("\s\s[transferring] '#{remote_path}'")
124
+ begin
125
+ cloud_io.upload(local_file.path, remote_path)
126
+ MUTEX.synchronize { @transfer_count += 1 }
127
+ rescue CloudIO::FileSizeError => err
128
+ MUTEX.synchronize { @skipped_count += 1 }
129
+ Logger.warn Error.wrap(err, "Skipping '#{remote_path}'")
130
+ rescue => err
131
+ Logger.error(err)
132
+ raise Error, <<-EOS
133
+ Syncer Failed!
134
+ See the Retry [info] and [error] messages (if any)
135
+ for details on each failed operation.
136
+ EOS
137
+ end
138
+ end
139
+ elsif remote_md5
140
+ @orphans << remote_path
141
+ end
142
+ end
143
+
144
+ def process_orphans
145
+ if @orphans.empty?
146
+ return mirror ? "Deleted Files: 0" : "Orphaned Files: 0"
147
+ end
148
+
149
+ if @orphans.is_a?(Queue)
150
+ @orphans = Array.new(@orphans.size) { @orphans.shift }
151
+ end
152
+
153
+ if mirror
154
+ Logger.info @orphans.map { |path|
155
+ "\s\s[removing] '#{path}'"
156
+ }.join("\n")
157
+
158
+ begin
159
+ cloud_io.delete(@orphans)
160
+ "Deleted Files: #{@orphans.count}"
161
+ rescue => err
162
+ Logger.warn Error.wrap(err, "Delete Operation Failed")
163
+ "Attempted to Delete: #{@orphans.count} " \
164
+ "(See log messages for actual results)"
165
+ end
166
+ else
167
+ Logger.info @orphans.map { |path|
168
+ "\s\s[orphaned] '#{path}'"
169
+ }.join("\n")
170
+ "Orphaned Files: #{@orphans.count}"
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end