summaryrefslogtreecommitdiff
path: root/lib/byteback/backup_directory.rb
diff options
context:
space:
mode:
authorMatthew Bloch <matthew@bytemark.co.uk>2014-11-05 14:00:45 +0000
committerMatthew Bloch <matthew@bytemark.co.uk>2014-11-05 14:00:45 +0000
commitc88333e19b459685f57ba3f246c3fdd684681542 (patch)
tree3be762cf0a9ebb57c8a8528bcf98433434d371ad /lib/byteback/backup_directory.rb
parentf6c155ff0b261f23b6fc9465b7f2e6d85bab573a (diff)
Split out -prune from -snapshot, tested the latter on various live setups.
Diffstat (limited to 'lib/byteback/backup_directory.rb')
-rw-r--r--lib/byteback/backup_directory.rb175
1 files changed, 111 insertions, 64 deletions
diff --git a/lib/byteback/backup_directory.rb b/lib/byteback/backup_directory.rb
index f0ceabc..e5ab8c0 100644
--- a/lib/byteback/backup_directory.rb
+++ b/lib/byteback/backup_directory.rb
@@ -1,13 +1,113 @@
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 = [0, 1, 2, 3, 7, 14, 21, 28, 56, 112]
+
+ def sort_by_importance(snapshots_unsorted, now=Time.now)
+ snapshots_sorted = []
+
+ # FIXME: takes about a minute to sort 900 items,
+ # seems like that ought to be quicker than O(n^2)
+ #
+ while !snapshots_unsorted.empty?
+ BACKUP_IMPORTANCE.each do |days|
+ target_time = now - (days*86400)
+ closest = snapshots_unsorted.inject(nil) do |best, snapshot|
+ if best.nil? || (snapshot.time-target_time).abs < (best.time-target_time).abs
+ snapshot
+ else
+ best
+ end
+ end
+ break unless closest
+ snapshots_sorted << snapshots_unsorted.delete(closest)
+ end
+ 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(path)
+ end
+
+ def <=>(b)
+ time <=> b.time
+ end
+
+ def create!(from)
+ system_no_error("btrfs subvolume snapshot #{from} #{path}")
+ end
+
+ def delete!
+ system_no_error("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
@@ -21,83 +121,30 @@ module Byteback
# Return an array of Times representing the current list of
# snapshots.
#
- def snapshot_times
+ 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
- Time.parse(entry)
- rescue ArgumentError => error
+ Snapshot.new(self, snapshot_path)
+ rescue ArgumentError => ae
+ # directory name must represent a parseable Time
nil
end
end.
- compact.
- sort
- end
-
- # 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 = [0, 1, 2, 3, 7, 14, 21, 28, 56, 112]
- def snapshot_times_by_importance
- now = Time.now
- snapshot_times_unsorted = snapshot_times
- snapshot_times_sorted = []
- while !snapshot_times_unsorted.empty?
- BACKUP_IMPORTANCE.each do |days|
- target_time = now + (days*86400)
- closest = snapshot_times_unsorted.inject(nil) do |best, time|
- if best.nil? || (time-target_time).abs < (best-target_time).abs
- time
- else
- best
- end
- end
- break unless closest
- snapshot_times_sorted << snapshot_times_unsorted.delete(closest)
- end
- end
- snapshot_times_sorted
- 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 snapshot_size(time=snapshot_times.last)
- `du -s -b #{snapshot_path(time)}`.to_i
- end
-
- def average_snapshot_size(number=10)
- snapshot_times.sort[0..number].inject(0) { |total, time| snapshot_size(time) } / number
+ compact
end
# Create a new snapshot of 'current'
#
- def new_snapshot!
- system_no_error("btrfs subvolume snapshot -r #{current.path} #{snapshot_path}")
- end
-
- def delete_snapshot!(time)
- system_no_error("btrfs subvolume delete #{snapshot_path(time)}")
+ def new_snapshot!(time = Time.now)
+ snapshot_path = time.strftime("%Y-%m-%dT%H:%M%z")
+ Snapshot.new(self, snapshot_path).create!(current.path)
end
def current
Dir.new("#{dir.path}/current")
end
-
- def snapshot_path(time=Time.now)
- "#{dir.path}/#{time.strftime("%Y-%m-%dT%H:%M%z")}"
- 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
end