lsync 1.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.
- 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
|