1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
module Byteback
# Represents a particular timestamped backup directory
class Snapshot
class << self
# What order to remove snapshots in to regain disk space?
#
# Order backups by their closeness to defined backup times, which are
# listed in a set order (i.e. today's backup is more important than yesterday's).
#
BACKUP_IMPORTANCE = [1, 2, 7, 14, 21, 28, 56, 112]
def sort_by_importance(snapshots_unsorted, now=Time.now)
snapshots_sorted = []
scores = Array.new{|h,k| h[k] = []}
times = snapshots_unsorted.map(&:time)
BACKUP_IMPORTANCE.each_with_index do |days, backup_idx|
target_time = now.to_i - (days*86400)
weight = days.to_f - (backup_idx == 0 ? 0 : BACKUP_IMPORTANCE[backup_idx-1])
scores << times.map{|t| (t.to_i - target_time).abs/weight }
end
#
# Find the index of the lowest score from the list of BACKUP_IMPORTANCE
#
nearest_target = scores.transpose.map{|s| s.find_index(s.min)}
BACKUP_IMPORTANCE.each_index do |backup_idx|
#
# Find the indicies of the snapshots that match the current BACKUP_IMPORTANCE index, and sort them according to their score.
best_snapshot_idxs = nearest_target.each_index.
select{|i| nearest_target[i] == backup_idx}.
sort{|a,b| scores[backup_idx][a] <=> scores[backup_idx][b]}
#
# Append them to the array.
#
snapshots_sorted += snapshots_unsorted.values_at(*best_snapshot_idxs)
end
snapshots_sorted
end
end
attr_reader :backup_directory, :path
def initialize(backup_directory, snapshot_path)
@backup_directory = backup_directory
@path = snapshot_path
time # throws ArgumentError if it can't parse
nil
end
def time
Time.parse(File.basename(path))
end
def <=>(b)
time <=> b.time
end
def create!(from)
system_no_error("/sbin/btrfs subvolume snapshot #{from} #{path}")
end
def delete!
system_no_error("/sbin/btrfs subvolume delete #{path}")
end
# Returns the size of the given snapshot (runs du, may be slow)
#
# Would much prefer to take advantage of this feature:
# http://dustymabe.com/2013/09/22/btrfs-how-big-are-my-snapshots/
# but it's not currently in Debian/wheezy.
#
def du
`du -s -b #{path}`.to_i
end
protected
def system_no_error(*args)
args[-1] += " > /dev/null" unless @verbose
raise RuntimeError.new("Command failed: "+args.join(" ")) unless
system(*args)
end
end
# Represent a directory full of backups where "current" is a subvolume
# which is snapshotted to frozen backup directories called e.g.
# "yyyy-mm-ddThh:mm+zzzz".
#
class BackupDirectory
class << self
# Return all backup directories
#
def all
Dir.new(ENV['HOME']).entries.map do |entry|
next if entry[0] == '.'
name = File.expand_path(ENV['HOME'] + "/" + entry)
File.directory?(name + "/current") ? BackupDirectory.new(name) : nil
end.
compact
end
# Returns every snapshot in every backup directory
#
def all_snapshots
all.map { |dir| dir.snapshots }.flatten
end
end
attr_reader :dir
def initialize(dir)
@dir = Dir.new(dir)
raise Errno::ENOENT unless File.directory?(dir)
current
end
# Return total amount of free space in backup directory (bytes)
#
def free
df = DiskFree.new(@dir.path)
df.total - df.used
end
# Return an array of Times representing the current list of
# snapshots.
#
def snapshots
@dir.entries.map do |entry|
next if entry[0] == '.' || entry == 'current'
snapshot_path = File.expand_path(@dir.path + "/" + entry)
next unless File.directory?(snapshot_path)
begin
Snapshot.new(self, snapshot_path)
rescue ArgumentError => ae
# directory name must represent a parseable Time
nil
end
end.
compact
end
# Create a new snapshot of 'current'
#
def new_snapshot!(time = Time.now)
snapshot_path = time.strftime(dir.path + "/%Y-%m-%dT%H:%M%z")
Snapshot.new(self, snapshot_path).create!(current.path)
end
def current
Dir.new("#{dir.path}/current")
end
end
end
|