summaryrefslogtreecommitdiff
path: root/lib/byteback
diff options
context:
space:
mode:
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