summaryrefslogtreecommitdiff
path: root/lib/byteback
diff options
context:
space:
mode:
authorPatrick J Cherry <patrick@bytemark.co.uk>2015-12-01 22:41:06 +0000
committerPatrick J Cherry <patrick@bytemark.co.uk>2015-12-01 22:41:06 +0000
commit668b9871e64cb82ac30c8defb29d56d774f3c140 (patch)
tree9b4bba7cbcbd2660485cf803402c4d4889e62b10 /lib/byteback
parent182a03798d49a3c0450b0f137977037cf9376e99 (diff)
Completely re-vamped restore command. Fixes #12403
The byteback-restore command now uses the rsync xattrs to display information about the files due to be restored. It can handle filenames with spaces (!) and other characters.
Diffstat (limited to 'lib/byteback')
-rw-r--r--lib/byteback/restore.rb122
-rw-r--r--lib/byteback/restore_file.rb215
2 files changed, 337 insertions, 0 deletions
diff --git a/lib/byteback/restore.rb b/lib/byteback/restore.rb
new file mode 100644
index 0000000..e2df3fe
--- /dev/null
+++ b/lib/byteback/restore.rb
@@ -0,0 +1,122 @@
+
+require 'byteback/restore_file'
+
+module Byteback
+
+ class Restore
+
+ def self.find(byteback_root, revision, paths)
+ x = Byteback::Restore.new(byteback_root)
+ x.revision = revision
+ x.find(paths)
+ return x
+ end
+
+ #
+ # This takes a string or array of strings as an argument, and QP encodes
+ # each argument. This is for safe parsing of spaces etc at the remote end.
+ #
+ # Returns an array of encoded strings.
+ #
+ def self.encode_args(args)
+ [args].flatten.collect{|s| [s].pack("M").gsub(" ","=20").gsub("=\n","")}
+ end
+
+ #
+ # This takes a string or array of strings, each of which is quoted
+ # printable, and unpacks it.
+ #
+ # Returns an array of decoded strings.
+ #
+ def self.decode_args(args)
+ [args].flatten.collect{|s| (s + "=\n").unpack("M")}.flatten
+ end
+
+ def initialize(byteback_root)
+ #
+ # We use expand_path here to make sure we have a full path, with no
+ # trailing slash.
+ #
+ @byteback_root = File.expand_path(byteback_root)
+ @now = Time.now
+ @revision = "*"
+ @results = []
+ end
+
+ def revision=(r)
+ if r =~ /^[a-z0-9:\+\*\-]+$/i
+ @revision = r
+ else
+ puts "*** Warning: Bad revision #{r.inspect}"
+ end
+ end
+
+ def results
+ @results
+ end
+
+ def find(paths, full = false)
+ results = []
+ #
+ # Make sure we've an array, and that we get rid of any ".." nonsense.
+ #
+ paths = [paths].flatten.collect{|p| File.expand_path(p, "/")}
+ seen = []
+
+ @results = paths.collect do |path|
+ Dir.glob(File.expand_path(File.join(@byteback_root, @revision, path))).collect do |f|
+ restore_file = Byteback::RestoreFile.new(f, @byteback_root, @now)
+ end
+ end.flatten.sort{|a,b| [a.path, a.snapshot_time] <=> [b.path, b.snapshot_time]}
+
+ #
+ # If we want an unpruned list, return it now.
+ #
+ return @results if full
+
+ pruned_results = []
+
+ @results.each do |r|
+ pruned_results << r unless pruned_results.include?(r)
+ end
+
+ @results = pruned_results
+ end
+
+ def list
+ heading = %w(snapshot modestring size uid gid mtime path)
+ listings = [heading]
+ @results.sort.each do |r|
+ listing = heading.collect{|m| r.__send__(m.to_sym).to_s }
+ if r.symlink?
+ listing[-1] << " -> "+r.readlink
+ end
+ listings << listing
+ end
+
+ field_sizes = [0]*heading.length
+
+ listings.each do |fields|
+ fields.each_with_index do |field, i|
+ field_sizes[i] = (field_sizes[i] > field.length) ? field_sizes[i] : field.length
+ end
+ end
+
+ fmt = field_sizes.collect{|i| "%-#{i}.#{i}s"}.join(" ")
+
+ bar = "-"*field_sizes.inject(field_sizes.length){|m,s| m+=s}
+
+ output = []
+ listings.each do |fields|
+ output << sprintf(fmt, *fields)
+ if bar
+ output << bar
+ bar = nil
+ end
+ end
+
+ return output.join("\n")
+ end
+
+ end
+end
diff --git a/lib/byteback/restore_file.rb b/lib/byteback/restore_file.rb
new file mode 100644
index 0000000..e9282d0
--- /dev/null
+++ b/lib/byteback/restore_file.rb
@@ -0,0 +1,215 @@
+require 'ffi-xattr'
+require 'scanf'
+require 'pp'
+
+module Byteback
+ class RestoreFile
+ S_IFMT = 0170000 # bit mask for the file type bit fields
+
+ S_IFSOCK = 0140000 # socket
+ S_IFLNK = 0120000 # symbolic link
+ S_IFREG = 0100000 # regular file
+ S_IFBLK = 0060000 # block device
+ S_IFDIR = 0040000 # directory
+ S_IFCHR = 0020000 # character device
+ S_IFIFO = 0010000 # FIFO
+
+ S_ISUID = 0004000 # set-user-ID bit
+ S_ISGID = 0002000 # set-group-ID bit (see below)
+ S_ISVTX = 0001000 # sticky bit (see below)
+
+ S_IRWXU = 00700 # mask for file owner permissions
+ S_IRUSR = 00400 # owner has read permission
+ S_IWUSR = 00200 # owner has write permission
+ S_IXUSR = 00100 # owner has execute permission
+
+ S_IRWXG = 00070 # mask for group permissions
+ S_IRGRP = 00040 # group has read permission
+ S_IWGRP = 00020 # group has write permission
+ S_IXGRP = 00010 # group has execute permission
+
+ S_IRWXO = 00007 # mask for permissions for others (not in group)
+ S_IROTH = 00004 # others have read permission
+ S_IWOTH = 00002 # others have write permission
+ S_IXOTH = 00001 # others have execute permission
+
+ include Comparable
+
+ def initialize(full_path, byteback_root=".", now = Time.now)
+ @full_path = full_path
+ @byteback_root = byteback_root
+ @now = now
+
+ #
+ # The snapshot is the first directory after the byteback_root
+ #
+ @snapshot = full_path.sub(%r(^#{Regexp.escape @byteback_root}),'').split("/")[1]
+
+ if @snapshot == "current"
+ @snapshot_time = @now
+ else
+ @snapshot_time = Time.parse(@snapshot)
+ end
+
+ #
+ # Restore path
+ #
+ @path = full_path.sub(%r(^#{Regexp.escape @byteback_root}/#{Regexp.escape @snapshot}),'')
+
+ @stat = @mode = @dev_major = @dev_minor = @uid = @gid = nil
+ end
+
+ def <=>(other)
+ [self.path, self.mtime.to_i, self.size] <=> [other.path, other.mtime.to_i, other.size]
+ end
+
+ def stat
+ @stat = ::File.lstat(@full_path) unless @stat.is_a?(File::Stat)
+ @stat
+ end
+
+ def snapshot
+ @snapshot
+ end
+
+ def snapshot_time
+ @snapshot_time
+ end
+
+ def path
+ @path
+ end
+
+ def to_s
+ sprintf("%10s %i %4i %4i %s %s %s", self.modestring, self.size, self.uid, self.gid, self.mtime.strftime("%b %2d %H:%M"), @snapshot, @path)
+ end
+
+ def read_rsync_xattrs
+ xattr = Xattr.new(@full_path, :no_follow => false)
+ rsync_xattrs = xattr["user.rsync.%stat"]
+ if rsync_xattrs
+ @mode, @dev_major, @dev_minor, @uid, @gid = rsync_xattrs.scanf("%o %d,%d %d:%d")
+ raise ArgumentError, "Corrupt rsync stat xattr found for #{@full_path} (#{rsync_xattrs})" unless [@mode, @dev_major, @dev_minor, @uid, @gid].all?{|i| i.is_a?(Integer)}
+ else
+ warn "No rsync stat xattr found for #{@full_path}"
+ @mode, @dev_major, @dev_minor, @uid, @gid = %w(mode dev_major dev_minor uid gid).collect{|m| self.stat.__send__(m.to_sym)}
+ end
+ end
+
+ def mode
+ return self.stat.mode if self.stat.symlink?
+ read_rsync_xattrs unless @mode
+ @mode
+ end
+
+ def dev_minor
+ read_rsync_xattrs unless @dev_minor
+ @dev_minor
+ end
+
+ def dev_major
+ read_rsync_xattrs unless @dev_major
+ @dev_major
+ end
+
+ def uid
+ read_rsync_xattrs unless @uid
+ @uid
+ end
+
+ def gid
+ read_rsync_xattrs unless @gid
+ @gid
+ end
+
+ #
+ # This returns the type of file as a single character.
+ #
+ def ftypelet
+ if file?
+ "-"
+ elsif directory?
+ "d"
+ elsif blockdev?
+ "b"
+ elsif chardev?
+ "c"
+ elsif symlink?
+ "l"
+ elsif fifo?
+ "p"
+ elsif socket?
+ "s"
+ else
+ "?"
+ end
+ end
+
+ #
+ # This returns a modestring from the octal, like drwxr-xr-x.
+ # This has mostly been copied from strmode from filemode.h in coreutils.
+ #
+ def modestring
+ str = ""
+ str << ftypelet
+ str << ((mode & S_IRUSR == S_IRUSR) ? 'r' : '-')
+ str << ((mode & S_IWUSR == S_IWUSR) ? 'w' : '-')
+ str << ((mode & S_ISUID == S_ISUID) ?
+ ((mode & S_IXUSR == S_IXUSR) ? 's' : 'S') :
+ ((mode & S_IXUSR == S_IXUSR) ? 'x' : '-'))
+ str << ((mode & S_IRGRP == S_IRGRP) ? 'r' : '-')
+ str << ((mode & S_IWGRP == S_IWGRP) ? 'w' : '-')
+ str << ((mode & S_ISGID == S_ISGID) ?
+ ((mode & S_IXGRP == S_IXGRP) ? 's' : 'S') :
+ ((mode & S_IXGRP == S_IXGRP) ? 'x' : '-'))
+ str << ((mode & S_IROTH == S_IROTH) ? 'r' : '-')
+ str << ((mode & S_IWOTH == S_IWOTH) ? 'w' : '-')
+ str << ((mode & S_ISVTX == S_ISVTX) ?
+ ((mode & S_IXOTH == S_IXOTH) ? 't' : 'T') :
+ ((mode & S_IXOTH == S_IXOTH) ? 'x' : '-'))
+ return str
+ end
+
+ def socket?
+ (mode & S_IFMT) == S_IFSOCK
+ end
+
+ def symlink?
+ self.stat.symlink? || (mode & S_IFMT) == S_IFLNK
+ end
+
+ def file?
+ (mode & S_IFMT) == S_IFREG
+ end
+
+ def blockdev?
+ (mode & S_IFMT) == S_IFBLK
+ end
+
+ def directory?
+ (mode & S_IFMT) == S_IFDIR
+ end
+
+ def chardev?
+ (mode & S_IFMT) == S_IFCHR
+ end
+
+ def fifo?
+ (mode & S_IFMT) == S_IFIFO
+ end
+
+ def readlink
+ if self.stat.symlink?
+ File.readlink(@full_path)
+ else
+ File.read(@full_path).chomp
+ end
+ end
+
+ def method_missing(m, *args, &blk)
+ return self.stat.__send__(m) if self.stat.respond_to?(m)
+
+ raise NoMethodError, m
+ end
+ end
+end