From b6639d4a277e55cbe4847a989d09c7e4dfa23683 Mon Sep 17 00:00:00 2001 From: Michael Vrable Date: Tue, 2 Oct 2007 11:54:37 -0700 Subject: [PATCH] Add a restore-snapshot command to lbs-util. This is still in development, and not fully tested yet. This is meant to mostly replace restore.pl, and will eventually have more features (it can already restore without needing all segments unpacked first). The restore.pl script won't go away, though, since it is still useful to include as a small, self-contained bare-bones restore program. --- NEWS | 2 ++ lbs-util | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- lbs.py | 65 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 134bd9e..01ac9b1 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,8 @@ features of the Perl implementation, plus some other new features. The Perl library and utility are deprecated, and will be removed in the future. + - Preliminary snapshot restore support in lbs-util. This is still + not yet extensively tested. 0.4 [2007-08-24] - Documentation improvements: a getting started README, and a diff --git a/lbs-util b/lbs-util index 4342601..267c955 100755 --- a/lbs-util +++ b/lbs-util @@ -2,7 +2,7 @@ # # Utility for managing LBS archives. -import getpass, os, sys +import getpass, os, stat, sys, time from optparse import OptionParser import lbs @@ -128,6 +128,93 @@ def cmd_verify_snapshots(snapshots): raise ValueError("Bad checksum found") store.cleanup() +# Restore a snapshot, or some subset of files from it +def cmd_restore_snapshot(args): + get_passphrase() + lowlevel = lbs.LowlevelDataStore(options.store) + store = lbs.ObjectStore(lowlevel) + snapshot = lbs.parse_full(store.load_snapshot(args[0])) + destdir = args[1] + paths = args[2:] + + def warn(m, msg): + print "Warning: %s: %s" % (m.items.name, msg) + + for m in lbs.iterate_metadata(store, snapshot['Root']): + pathname = os.path.normpath(m.items.name) + while os.path.isabs(pathname): + pathname = pathname[1:] + print pathname + destpath = os.path.join(destdir, pathname) + (path, filename) = os.path.split(destpath) + + # TODO: Check for ../../../paths that might attempt to write outside + # the destination directory. Maybe also check attempts to follow + # symlinks pointing outside? + + try: + if not os.path.isdir(path): + os.makedirs(path) + + if m.items.type == '-': + file = open(destpath, 'wb') + verifier = lbs.ChecksumVerifier(m.items.checksum) + size = 0 + for block in m.data(): + data = store.get(block) + verifier.update(data) + size += len(data) + file.write(data) + file.close() + if int(m.fields['size']) != size: + raise ValueError("File size does not match!") + if not verifier.valid(): + raise ValueError("Bad checksum found") + elif m.items.type == 'd': + if filename != '.': + os.mkdir(destpath) + elif m.items.type == 'l': + os.symlink(m.items.contents, destpath) + elif m.items.type == 'p': + os.mkfifo(destpath) + elif m.items.type in ('c', 'b'): + if m.items.type == 'c': + mode = 0600 | stat.S_IFCHR + else: + mode = 0600 | stat.S_IFBLK + os.mknod(destpath, mode, os.makedev(*m.items.device)) + elif m.items.type == 's': + pass # TODO: Implement + else: + warn(m, "Unknown type code: " + m.items.type) + continue + + except Exception, e: + warn(m, "Error restoring: %s" % (e,)) + continue + + try: + uid = m.items.user[0] + gid = m.items.group[0] + os.lchown(destpath, uid, gid) + except Exception, e: + warn(m, "Error restoring file ownership: %s" % (e,)) + + if m.items.type == 'l': + continue + + try: + os.chmod(destpath, m.items.mode) + except Exception, e: + warn(m, "Error restoring file permissions: %s" % (e,)) + + try: + os.utime(destpath, (time.time(), m.items.mtime)) + except Exception, e: + warn(m, "Error restoring file timestamps: %s" % (e,)) + + store.cleanup() + if len(args) == 0: parser.print_usage() sys.exit(1) @@ -147,6 +234,8 @@ elif cmd == 'list-snapshot-sizes': cmd_list_snapshot_sizes() elif cmd == 'verify-snapshots': cmd_verify_snapshots(args) +elif cmd == 'restore-snapshot': + cmd_restore_snapshot(args) else: print "Unknown command:", cmd parser.print_usage() diff --git a/lbs.py b/lbs.py index f0cae55..7eab075 100644 --- a/lbs.py +++ b/lbs.py @@ -298,11 +298,60 @@ def read_metadata(object_store, root): class MetadataItem: """Metadata for a single file (or directory or...) from a snapshot.""" + # Functions for parsing various datatypes that can appear in a metadata log + # item. + @staticmethod + def decode_int(s): + """Decode an integer, expressed in decimal, octal, or hexadecimal.""" + if s.startswith("0x"): + return int(s, 16) + elif s.startswith("0"): + return int(s, 8) + else: + return int(s, 10) + + @staticmethod + def decode_str(s): + """Decode a URI-encoded (%xx escapes) string.""" + def hex_decode(m): return chr(int(m.group(1), 16)) + return re.sub(r"%([0-9a-f]{2})", hex_decode, s) + + @staticmethod + def raw_str(s): + """An unecoded string.""" + return s + + @staticmethod + def decode_user(s): + """Decode a user/group to a tuple of uid/gid followed by name.""" + items = s.split() + uid = MetadataItem.decode_int(items[0]) + name = None + if len(items) > 1: + if items[1].startswith("(") and items[1].endswith(")"): + name = MetadataItem.decode_str(items[1][1:-1]) + return (uid, name) + + @staticmethod + def decode_device(s): + """Decode a device major/minor number.""" + (major, minor) = map(MetadataItem.decode_int, s.split("/")) + return (major, minor) + + class Items: pass + def __init__(self, fields, object_store): """Initialize from a dictionary of key/value pairs from metadata log.""" self.fields = fields self.object_store = object_store + self.keys = [] + self.items = self.Items() + for (k, v) in fields.items(): + if k in self.field_types: + decoder = self.field_types[k] + setattr(self.items, k, decoder(v)) + self.keys.append(k) def data(self): """Return an iterator for the data blocks that make up a file.""" @@ -334,6 +383,22 @@ class MetadataItem: else: yield ref +# Description of fields that might appear, and how they should be parsed. +MetadataItem.field_types = { + 'name': MetadataItem.decode_str, + 'type': MetadataItem.raw_str, + 'mode': MetadataItem.decode_int, + 'device': MetadataItem.decode_device, + 'user': MetadataItem.decode_user, + 'group': MetadataItem.decode_user, + 'mtime': MetadataItem.decode_int, + 'links': MetadataItem.decode_int, + 'inode': MetadataItem.raw_str, + 'checksum': MetadataItem.decode_str, + 'size': MetadataItem.decode_int, + 'contents': MetadataItem.decode_str, +} + def iterate_metadata(object_store, root): for d in parse(read_metadata(object_store, root), lambda l: len(l) == 0): yield MetadataItem(d, object_store) -- 2.20.1