lsync 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/lsync +152 -0
- data/lib/lsync.rb +40 -0
- data/lib/lsync/action.rb +100 -0
- data/lib/lsync/actions/darwin/disk +19 -0
- data/lib/lsync/actions/darwin/terminal +8 -0
- data/lib/lsync/actions/generic/prune +251 -0
- data/lib/lsync/actions/generic/rotate +75 -0
- data/lib/lsync/actions/linux/disk +28 -0
- data/lib/lsync/actions/linux/terminal +6 -0
- data/lib/lsync/backup_error.rb +33 -0
- data/lib/lsync/backup_plan.rb +249 -0
- data/lib/lsync/backup_script.rb +136 -0
- data/lib/lsync/directory.rb +39 -0
- data/lib/lsync/extensions.rb +22 -0
- data/lib/lsync/lb.py +1304 -0
- data/lib/lsync/method.rb +191 -0
- data/lib/lsync/password.rb +35 -0
- data/lib/lsync/run.rb +34 -0
- data/lib/lsync/server.rb +94 -0
- data/lib/lsync/shell.rb +103 -0
- data/lib/lsync/shell_client.rb +84 -0
- data/lib/lsync/tee_logger.rb +37 -0
- data/lib/lsync/version.rb +24 -0
- metadata +131 -0
data/bin/lsync
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'ftools'
|
4
|
+
require 'lsync'
|
5
|
+
require 'lsync/version'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
OPTIONS = {
|
9
|
+
:ConfigFiles => [],
|
10
|
+
:Plan => nil,
|
11
|
+
:LogPath => "/var/log/lsync/backup.log"
|
12
|
+
}
|
13
|
+
|
14
|
+
ARGV.options do |o|
|
15
|
+
script_name = File.basename($0)
|
16
|
+
|
17
|
+
o.set_summary_indent(' ')
|
18
|
+
o.banner = "Usage: #{script_name} [options] [directory | config]"
|
19
|
+
o.define_head "This script is used to run backups. If you specify a directory, files ending in .conf will all be processed."
|
20
|
+
|
21
|
+
o.separator ""
|
22
|
+
o.separator "Help and Copyright information"
|
23
|
+
|
24
|
+
o.on("-p plan", String, "Run the specified backup plan") { |plan| OPTIONS[:Plan] = plan }
|
25
|
+
o.on("-l log_path", String, "Set the directory for backup logs") { |log_path| OPTIONS[:LogPath] = log_path }
|
26
|
+
|
27
|
+
o.on_tail("--copy", "Display copyright information") do
|
28
|
+
puts "#{script_name} v#{LSync::VERSION::STRING}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
|
29
|
+
puts "See http://www.oriontransfer.co.nz/ for more information."
|
30
|
+
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
|
34
|
+
o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
|
35
|
+
end.parse!
|
36
|
+
|
37
|
+
# ============================== Main Scripts / Plans ==============================
|
38
|
+
|
39
|
+
class LogPathChecker
|
40
|
+
def initialize(directory, filename)
|
41
|
+
full_path = File.join(directory, filename)
|
42
|
+
|
43
|
+
@directory_writable = File.directory?(directory) && File.writable?(directory)
|
44
|
+
@file_not_writable = File.exist?(full_path) && !File.writable?(full_path)
|
45
|
+
|
46
|
+
@accessable = @directory_writable && !@file_not_writable
|
47
|
+
@full_path = full_path
|
48
|
+
end
|
49
|
+
|
50
|
+
attr :accessable
|
51
|
+
attr :full_path
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup_logging
|
55
|
+
# Console log output
|
56
|
+
$console_log = Logger.new($stdout)
|
57
|
+
$console_log.formatter = MinimalLogFormat.new
|
58
|
+
|
59
|
+
# File log output
|
60
|
+
filename = File.basename(OPTIONS[:LogPath])
|
61
|
+
directory = File.dirname(OPTIONS[:LogPath])
|
62
|
+
|
63
|
+
if filename == nil || filename == ""
|
64
|
+
filename = "backup.log"
|
65
|
+
end
|
66
|
+
|
67
|
+
check = LogPathChecker.new(directory, filename)
|
68
|
+
unless check.accessable
|
69
|
+
$console_log.warn "Could not write to #{check.full_path.dump} - changing log path to #{Dir.pwd.dump}"
|
70
|
+
|
71
|
+
directory = Dir.pwd
|
72
|
+
check = LogPathChecker.new(directory, filename)
|
73
|
+
end
|
74
|
+
|
75
|
+
if check.accessable
|
76
|
+
$backup_log = Logger.new(check.full_path, 'weekly')
|
77
|
+
else
|
78
|
+
$console_log.warn "Could not write to #{check.full_path.dump} - not logging to disk"
|
79
|
+
$backup_log = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
$logger = TeeLogger.new($console_log, $backup_log)
|
83
|
+
end
|
84
|
+
|
85
|
+
def process_config_files
|
86
|
+
ARGV.each do |p|
|
87
|
+
unless File.exists? p
|
88
|
+
$logger.error "Could not process #{p}"
|
89
|
+
exit(20)
|
90
|
+
end
|
91
|
+
|
92
|
+
if File.directory? p
|
93
|
+
OPTIONS[:ConfigFiles] += Dir[File.join(p, "*.conf")]
|
94
|
+
else
|
95
|
+
OPTIONS[:ConfigFiles] << p
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_options
|
101
|
+
if OPTIONS[:Plan] && OPTIONS[:ConfigFiles].size > 0
|
102
|
+
$logger.error "Please specify either a backup plan or a set of backup scripts, but not both in one run."
|
103
|
+
exit(27)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def run_backups
|
108
|
+
OPTIONS[:ConfigFiles].each do |c|
|
109
|
+
config = LSync::BackupScript.load_from_file(c)
|
110
|
+
config.logger = $logger
|
111
|
+
config.run_backup
|
112
|
+
end
|
113
|
+
|
114
|
+
if OPTIONS[:Plan]
|
115
|
+
config = LSync::BackupPlan.load_from_file(OPTIONS[:Plan])
|
116
|
+
config.logger = $logger
|
117
|
+
config.run_backup
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def dump_exception_backtrace ex
|
122
|
+
ex.backtrace.each do |bt|
|
123
|
+
$logger.error bt
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
setup_logging
|
128
|
+
|
129
|
+
process_config_files
|
130
|
+
|
131
|
+
validate_options
|
132
|
+
|
133
|
+
$logger.info " Backup beginning at #{Time.now.to_s} ".center(96, "=")
|
134
|
+
|
135
|
+
begin
|
136
|
+
run_backups
|
137
|
+
rescue LSync::BackupError
|
138
|
+
$logger.error "#{$!.class.name}: #{$!.to_s}. Dumping backtrace:"
|
139
|
+
dump_exception_backtrace($!)
|
140
|
+
exit(230)
|
141
|
+
rescue Interrupt
|
142
|
+
$logger.error "Backup process recevied interrupt (Ctrl-C). Dumping backtrace:"
|
143
|
+
dump_exception_backtrace($!)
|
144
|
+
exit(240)
|
145
|
+
rescue
|
146
|
+
$logger.error "Unknown exception #{$!.class.name} thrown: #{$!.to_s}. Dumping backtrace:"
|
147
|
+
dump_exception_backtrace($!)
|
148
|
+
exit(250)
|
149
|
+
ensure
|
150
|
+
$logger.info " Backup finishing at #{Time.now.to_s} ".center(96, "=")
|
151
|
+
end
|
152
|
+
|
data/lib/lsync.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Copyright (c) 2007 Samuel Williams. Released under the GNU GPLv2.
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require 'rubygems'
|
17
|
+
|
18
|
+
gem 'termios'
|
19
|
+
gem 'net-ssh'
|
20
|
+
gem 'ruleby'
|
21
|
+
|
22
|
+
require 'yaml'
|
23
|
+
require 'socket'
|
24
|
+
require 'set'
|
25
|
+
require 'logger'
|
26
|
+
|
27
|
+
require 'lsync/version'
|
28
|
+
require 'lsync/extensions'
|
29
|
+
require 'lsync/backup_script'
|
30
|
+
require 'lsync/backup_plan'
|
31
|
+
require 'lsync/tee_logger'
|
32
|
+
|
33
|
+
require 'fileutils'
|
34
|
+
require 'optparse'
|
35
|
+
|
36
|
+
require 'open-uri'
|
37
|
+
|
38
|
+
module LSync
|
39
|
+
|
40
|
+
end
|
data/lib/lsync/action.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
|
2
|
+
require 'pathname'
|
3
|
+
require 'lsync/run'
|
4
|
+
require 'lsync/backup_error'
|
5
|
+
|
6
|
+
module LSync
|
7
|
+
|
8
|
+
class AbortBackupException < Exception
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
class Action
|
13
|
+
def initialize(function)
|
14
|
+
@function = function
|
15
|
+
|
16
|
+
if @function.match(/^\%([a-z]+)(\s+.*)?$/)
|
17
|
+
@script_name = $1
|
18
|
+
@arguments = $2
|
19
|
+
else
|
20
|
+
@script_name = nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
@function
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_on_server(server, logger)
|
29
|
+
logger.info "Running #{@function} on #{server}"
|
30
|
+
|
31
|
+
if server.is_local?
|
32
|
+
run_locally(server, logger)
|
33
|
+
else
|
34
|
+
run_remotely(server, logger)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def run_locally(server, logger)
|
40
|
+
command = nil
|
41
|
+
|
42
|
+
if @script_name
|
43
|
+
uname = `uname`.chomp.downcase
|
44
|
+
|
45
|
+
local_path = Action.script_path(uname, @script_name)
|
46
|
+
command = local_path.to_cmd + @arguments
|
47
|
+
else
|
48
|
+
command = @function
|
49
|
+
end
|
50
|
+
|
51
|
+
ret = nil
|
52
|
+
Dir.chdir(server.root_path) do
|
53
|
+
ret = LSync.run_command(command, logger)
|
54
|
+
end
|
55
|
+
|
56
|
+
case(ret)
|
57
|
+
when 0
|
58
|
+
return
|
59
|
+
when 1
|
60
|
+
raise AbortBackupException
|
61
|
+
else
|
62
|
+
raise BackupActionError
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def run_remotely(server, logger)
|
67
|
+
conn = server.connect
|
68
|
+
conn.send_object([:set_working_dir, server.root_path])
|
69
|
+
|
70
|
+
if @script_name
|
71
|
+
uname = `uname`.chomp.downcase
|
72
|
+
|
73
|
+
local_path = Action.script_path(uname, @script_name)
|
74
|
+
|
75
|
+
logger.info("Sending run_script #{@script_name}...")
|
76
|
+
conn.send_object([:run_script, @script_name, Pathname.new(local_path).read, @arguments])
|
77
|
+
else
|
78
|
+
logger.info("Sending run_command #{@function}...")
|
79
|
+
conn.send_object([:run_command, @function])
|
80
|
+
end
|
81
|
+
|
82
|
+
conn.run do |message|
|
83
|
+
break if message == :done
|
84
|
+
|
85
|
+
logger.send(*message)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.script_path(platform, name)
|
90
|
+
exact_script_path(platform, name) || exact_script_path("generic", name)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
def self.exact_script_path(platform, name)
|
95
|
+
path = (Pathname.new(__FILE__).dirname + "actions" + platform + name).expand_path
|
96
|
+
path.exist? ? path : nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
DISKUTIL = "diskutil"
|
4
|
+
action = ARGV[0]
|
5
|
+
disk_name = ARGV[1]
|
6
|
+
|
7
|
+
def get_disk_id(name)
|
8
|
+
begin
|
9
|
+
`diskutil list`.match(/#{name}\s*\*?[0-9]+\.[0-9]+ .B\s+(disk[0-9]s[0-9])$/)[1]
|
10
|
+
rescue
|
11
|
+
exit 5
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if (action == 'mountpoint')
|
16
|
+
puts File.join('', 'Volumes', disk_name, ARGV[2..-1])
|
17
|
+
else
|
18
|
+
system DISKUTIL, action, get_disk_id(disk_name)
|
19
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This script takes a specified directory, and removes any directories that
|
4
|
+
# don't match the supplied policy. Thanks to Scott Lu and his "snapfilter"
|
5
|
+
# command which made me realise how complicated my first attempt at doing this
|
6
|
+
# was.
|
7
|
+
|
8
|
+
require 'pathname'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'optparse'
|
11
|
+
|
12
|
+
require 'set'
|
13
|
+
require 'time'
|
14
|
+
# Required for strptime
|
15
|
+
require 'date'
|
16
|
+
|
17
|
+
class Rotation
|
18
|
+
def initialize(path, time)
|
19
|
+
@path = path
|
20
|
+
@time = time
|
21
|
+
end
|
22
|
+
|
23
|
+
attr :time
|
24
|
+
attr :path
|
25
|
+
|
26
|
+
# Sort in reverse order by default
|
27
|
+
def <=> other
|
28
|
+
return other.time <=> @time
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Period
|
33
|
+
KeepOldest = Proc.new do |t1, t2|
|
34
|
+
t1 > t2
|
35
|
+
end
|
36
|
+
|
37
|
+
KeepYoungest = Proc.new do |t1, t2|
|
38
|
+
t1 < t2
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(count)
|
42
|
+
@count = count
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter(values, options = {})
|
46
|
+
slots = {}
|
47
|
+
|
48
|
+
keep = (options[:keep] == :youngest) ? KeepYoungest : KeepOldest
|
49
|
+
|
50
|
+
values.each do |value|
|
51
|
+
time = value.time
|
52
|
+
|
53
|
+
k = key(time)
|
54
|
+
|
55
|
+
# We want to keep the newest backup if possible (<).
|
56
|
+
next if slots.key?(k) and keep.call(value.time, slots[k].time)
|
57
|
+
|
58
|
+
slots[k] = value
|
59
|
+
end
|
60
|
+
|
61
|
+
sorted_values = slots.values.sort
|
62
|
+
|
63
|
+
return sorted_values[0...@count]
|
64
|
+
end
|
65
|
+
|
66
|
+
def key(t)
|
67
|
+
raise ArgumentError
|
68
|
+
end
|
69
|
+
|
70
|
+
def mktime(year, month=1, day=1, hour=0, minute=0, second=0)
|
71
|
+
return Time.gm(year, month, day, hour, minute, second)
|
72
|
+
end
|
73
|
+
|
74
|
+
attr :count
|
75
|
+
end
|
76
|
+
|
77
|
+
class Hourly < Period
|
78
|
+
def key(t)
|
79
|
+
mktime(t.year, t.month, t.day, t.hour)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Daily < Period
|
84
|
+
def key(t)
|
85
|
+
mktime(t.year, t.month, t.day)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Weekly < Period
|
90
|
+
def key(t)
|
91
|
+
mktime(t.year, t.month, t.day) - (t.wday * 3600 * 24)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class Monthly < Period
|
96
|
+
def key(t)
|
97
|
+
mktime(t.year, t.month)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Quarterly < Period
|
102
|
+
def key(t)
|
103
|
+
mktime(t.year, (t.month - 1) / 3 * 3 + 1)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Yearly < Period
|
108
|
+
def key(t)
|
109
|
+
mktime(t.year)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Policy
|
114
|
+
def initialize
|
115
|
+
@periods = {}
|
116
|
+
end
|
117
|
+
|
118
|
+
def <<(period)
|
119
|
+
@periods[period.class] = period
|
120
|
+
end
|
121
|
+
|
122
|
+
def filter(values, options = {})
|
123
|
+
filtered_values = Set.new
|
124
|
+
@periods.values.each do |period|
|
125
|
+
filtered_values += period.filter(values, options)
|
126
|
+
end
|
127
|
+
|
128
|
+
return filtered_values.to_a, (Set.new(values) - filtered_values).to_a
|
129
|
+
end
|
130
|
+
|
131
|
+
attr :periods
|
132
|
+
end
|
133
|
+
|
134
|
+
OPTIONS = {
|
135
|
+
:Format => "%Y.%m.%d-%H.%M.%S",
|
136
|
+
:Destination => "./*",
|
137
|
+
:Policy => Policy.new,
|
138
|
+
:PolicyOptions => {},
|
139
|
+
:Wet => true
|
140
|
+
}
|
141
|
+
|
142
|
+
ARGV.options do |o|
|
143
|
+
script_name = File.basename($0)
|
144
|
+
|
145
|
+
o.set_summary_indent(' ')
|
146
|
+
o.banner = "Usage: #{script_name} [options] [directory]"
|
147
|
+
o.define_head "This script is used to prune old backups."
|
148
|
+
|
149
|
+
o.separator ""
|
150
|
+
o.separator "Help and Copyright information"
|
151
|
+
|
152
|
+
o.on("-f format", String, "Set the format of the rotated directory names. See Time$strftime") do |format|
|
153
|
+
OPTIONS[:Format] = format
|
154
|
+
end
|
155
|
+
|
156
|
+
o.on("-d destination", String, "Set the directory that contains backups to prune.") do |destination|
|
157
|
+
OPTIONS[:Destination] = destination
|
158
|
+
end
|
159
|
+
|
160
|
+
o.on("--dry-run", "Print out what would be deleted, but don't actually delete anything.") do
|
161
|
+
OPTIONS[:Wet] = false
|
162
|
+
end
|
163
|
+
|
164
|
+
o.separator ""
|
165
|
+
|
166
|
+
o.on("--default-policy", "Sets up a typical policy retaining a reasonable number of rotations for up to 20 years.") do
|
167
|
+
OPTIONS[:Policy] << Hourly.new(24)
|
168
|
+
OPTIONS[:Policy] << Daily.new(7*4)
|
169
|
+
OPTIONS[:Policy] << Weekly.new(52)
|
170
|
+
OPTIONS[:Policy] << Monthly.new(12*3)
|
171
|
+
OPTIONS[:Policy] << Quarterly.new(4*10)
|
172
|
+
OPTIONS[:Policy] << Yearly.new(20)
|
173
|
+
end
|
174
|
+
|
175
|
+
o.separator ""
|
176
|
+
|
177
|
+
o.on("--keep-oldest", "Keep older backups within the same period divsion (the default).") do
|
178
|
+
OPTIONS[:PolicyOptions][:keep] = :oldest
|
179
|
+
end
|
180
|
+
|
181
|
+
o.on("--keep-youngest", "Keep younger backups within the same period division.") do
|
182
|
+
OPTIONS[:PolicyOptions][:keep] = :youngest
|
183
|
+
end
|
184
|
+
|
185
|
+
o.on("--hourly count", Integer, "Set the number of hourly backups to keep.") do |count|
|
186
|
+
OPTIONS[:Policy] << Hourly.new(count)
|
187
|
+
end
|
188
|
+
|
189
|
+
o.on("--daily count", Integer, "Set the number of daily backups to keep.") do |count|
|
190
|
+
OPTIONS[:Policy] << Daily.new(count)
|
191
|
+
end
|
192
|
+
|
193
|
+
o.on("--weekly count", Integer, "Set the number of weekly backups to keep.") do |count|
|
194
|
+
OPTIONS[:Policy] << Weekly.new(count)
|
195
|
+
end
|
196
|
+
|
197
|
+
o.on("--monthly count", Integer, "Set the number of monthly backups to keep.") do |count|
|
198
|
+
OPTIONS[:Policy] << Monthly.new(count)
|
199
|
+
end
|
200
|
+
|
201
|
+
o.on("--quaterly count", Integer, "Set the number of monthly backups to keep.") do |count|
|
202
|
+
OPTIONS[:Policy] << Quarterly.new(count)
|
203
|
+
end
|
204
|
+
|
205
|
+
o.on("--yearly count", Integer, "Set the number of monthly backups to keep.") do |count|
|
206
|
+
OPTIONS[:Policy] << Yearly.new(count)
|
207
|
+
end
|
208
|
+
|
209
|
+
o.separator ""
|
210
|
+
|
211
|
+
o.on_tail("--copy", "Display copyright information") do
|
212
|
+
puts "#{script_name}. Copyright (c) 2008-2009 Samuel Williams. Released under the GPLv3."
|
213
|
+
puts "See http://www.oriontransfer.co.nz/ for more information."
|
214
|
+
|
215
|
+
exit
|
216
|
+
end
|
217
|
+
|
218
|
+
o.on_tail("-h", "--help", "Show this help message.") do
|
219
|
+
puts o
|
220
|
+
exit
|
221
|
+
end
|
222
|
+
end.parse!
|
223
|
+
|
224
|
+
backups = []
|
225
|
+
|
226
|
+
Dir[OPTIONS[:Destination]].each do |path|
|
227
|
+
next if path.match("latest")
|
228
|
+
date_string = File.basename(path)
|
229
|
+
|
230
|
+
begin
|
231
|
+
backups << Rotation.new(path, DateTime.strptime(date_string, OPTIONS[:Format]))
|
232
|
+
rescue ArgumentError
|
233
|
+
puts "Skipping #{path}, error parsing #{date_string}: #{$!}"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
keep, erase = OPTIONS[:Policy].filter(backups)
|
238
|
+
|
239
|
+
if OPTIONS[:Wet]
|
240
|
+
erase.sort.each do |backup|
|
241
|
+
puts "Erasing #{backup.path}..."
|
242
|
+
$stdout.flush
|
243
|
+
FileUtils.rm_rf(backup.path)
|
244
|
+
end
|
245
|
+
else
|
246
|
+
puts "*** Dry Run ***"
|
247
|
+
puts "\tKeeping:"
|
248
|
+
keep.sort.each { |backup| puts "\t\t#{backup.path}" }
|
249
|
+
puts "\tErasing:"
|
250
|
+
erase.sort.each { |backup| puts "\t\t#{backup.path}" }
|
251
|
+
end
|