From 886dba8a2915252a008c4218627297d54eb1221a Mon Sep 17 00:00:00 2001 From: bescoto Date: Mon, 25 Aug 2003 06:58:33 +0000 Subject: ACL/EA parsing fixes, use new quoting style git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup/trunk@403 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109 --- rdiff-backup/CHANGELOG | 4 ++ rdiff-backup/rdiff_backup/cmodule.c | 129 +++++++++++++++++++++++++++++++++- rdiff-backup/rdiff_backup/eas_acls.py | 59 +++++++++------- rdiff-backup/rdiff_backup/metadata.py | 74 +++++++++---------- rdiff-backup/rdiff_backup/rpath.py | 21 +++--- rdiff-backup/testing/ctest.py | 8 +++ rdiff-backup/testing/eas_aclstest.py | 122 +++++++++++++++++++++++++++++++- 7 files changed, 340 insertions(+), 77 deletions(-) diff --git a/rdiff-backup/CHANGELOG b/rdiff-backup/CHANGELOG index 02c093c..11d316e 100644 --- a/rdiff-backup/CHANGELOG +++ b/rdiff-backup/CHANGELOG @@ -12,6 +12,10 @@ If there is data missing from the destination dir (for instance if a user mistakenly deletes it), only warn when restoring, instead of exiting with error. +Fixed bug in EA/ACL restoring, noticed by Greg Freemyer. Also updated +quoting of filenames and extended attributes names to match +forthcoming attr/facl utilities. + New in v0.13.1 (2003/08/08) --------------------------- diff --git a/rdiff-backup/rdiff_backup/cmodule.c b/rdiff-backup/rdiff_backup/cmodule.c index 30aac47..a5f1c65 100644 --- a/rdiff-backup/rdiff_backup/cmodule.c +++ b/rdiff-backup/rdiff_backup/cmodule.c @@ -27,6 +27,7 @@ #include #include + /* Some of the following code to define major/minor taken from code by * Jörg Schilling's star archiver. */ @@ -202,7 +203,6 @@ static PyObject *c_make_file_dict(self, args) return return_val; } - /* Convert python long into 7 byte string */ static PyObject *long2str(self, args) PyObject *self; @@ -247,12 +247,138 @@ static PyObject *str2long(self, args) } +/* --------------------------------------------------------------------- * + * This section is still GPL'd, but was copied from the libmisc + * section of getfacl by Andreas Gruenbacher + * . I'm just copying the code to + * preserve quoting compatibility between (get|set)f(acl|attr) and + * rdiff-backup. Taken on 8/24/2003. + * --------------------------------------------------------------------- */ + +#include +#include +#include + +int high_water_alloc(void **buf, size_t *bufsize, size_t newsize) +{ +#define CHUNK_SIZE 256 + /* + * Goal here is to avoid unnecessary memory allocations by + * using static buffers which only grow when necessary. + * Size is increased in fixed size chunks (CHUNK_SIZE). + */ + if (*bufsize < newsize) { + void *newbuf; + + newsize = (newsize + CHUNK_SIZE-1) & ~(CHUNK_SIZE-1); + newbuf = realloc(*buf, newsize); + if (!newbuf) + return 1; + + *buf = newbuf; + *bufsize = newsize; + } + return 0; +} + +const char *quote(const char *str) +{ + static char *quoted_str; + static size_t quoted_str_len; + const unsigned char *s; + char *q; + size_t nonpr; + + if (!str) + return str; + + for (nonpr = 0, s = (unsigned char *)str; *s != '\0'; s++) + if (!isprint(*s) || isspace(*s) || *s == '\\' || *s == '=') + nonpr++; + if (nonpr == 0) + return str; + + if (high_water_alloc((void **)"ed_str, "ed_str_len, + nonpr * 3 + 1)) + return NULL; + for (s = (unsigned char *)str, q = quoted_str; *s != '\0'; s++) { + if (!isprint(*s) || isspace(*s) || *s == '\\' || *s == '=') { + *q++ = '\\'; + *q++ = '0' + ((*s >> 6) ); + *q++ = '0' + ((*s >> 3) & 7); + *q++ = '0' + ((*s ) & 7); + } else + *q++ = *s; + } + *q++ = '\0'; + + return quoted_str; +} + +char *unquote(char *str) +{ + unsigned char *s, *t; + + if (!str) + return str; + + for (s = (unsigned char *)str; *s != '\0'; s++) + if (*s == '\\') + break; + if (*s == '\0') + return str; + +#define isoctal(c) \ + ((c) >= '0' && (c) <= '7') + + t = s; + do { + if (*s == '\\' && + isoctal(*(s+1)) && isoctal(*(s+2)) && isoctal(*(s+3))) { + *t++ = ((*(s+1) - '0') << 6) + + ((*(s+2) - '0') << 3) + + ((*(s+3) - '0') ); + s += 3; + } else + *t++ = *s; + } while (*s++ != '\0'); + + return str; +} + +/* ------------- End Gruenbach section --------------------------------- */ + +/* Translate quote above into python */ +static PyObject *acl_quote(PyObject *self, PyObject *args) +{ + char *s; + + if (!PyArg_ParseTuple(args, "s", &s)) return NULL; + return Py_BuildValue("s", quote(s)); +} + +/* Translate unquote above into python */ +static PyObject *acl_unquote(PyObject *self, PyObject *args) +{ + char *s; + + if (!PyArg_ParseTuple(args, "s", &s)) return NULL; + return Py_BuildValue("s", unquote(s)); +} + + +/* ------------- Python export lists -------------------------------- */ + static PyMethodDef CMethods[] = { {"make_file_dict", c_make_file_dict, METH_VARARGS, "Make dictionary from file stat"}, {"long2str", long2str, METH_VARARGS, "Convert python long to 7 byte string"}, {"str2long", str2long, METH_VARARGS, "Convert 7 byte string to python long"}, {"sync", my_sync, METH_VARARGS, "sync buffers to disk"}, + {"acl_quote", acl_quote, METH_VARARGS, + "Quote string, escaping non-printables"}, + {"acl_unquote", acl_unquote, METH_VARARGS, + "Unquote string, producing original input to quote"}, {NULL, NULL, 0, NULL} }; @@ -266,4 +392,3 @@ void initC(void) NULL, NULL); PyDict_SetItemString(d, "UnknownFileTypeError", UnknownFileTypeError); } - diff --git a/rdiff-backup/rdiff_backup/eas_acls.py b/rdiff-backup/rdiff_backup/eas_acls.py index 4b4d169..e543597 100644 --- a/rdiff-backup/rdiff_backup/eas_acls.py +++ b/rdiff-backup/rdiff_backup/eas_acls.py @@ -30,8 +30,7 @@ from __future__ import generators import base64, errno, re try: import posix1e except ImportError: pass -import static, Globals, metadata, connection, rorpiter, log - +import static, Globals, metadata, connection, rorpiter, log, C, rpath class ExtendedAttributes: """Hold a file's extended attribute information""" @@ -104,12 +103,12 @@ def ea_compare_rps(rp1, rp2): def EA2Record(ea): """Convert ExtendedAttributes object to text record""" - str_list = ['# file: %s' % ea.get_indexpath()] + str_list = ['# file: %s' % C.acl_quote(ea.get_indexpath())] for (name, val) in ea.attr_dict.iteritems(): if not val: str_list.append(name) else: encoded_val = base64.encodestring(val).replace('\n', '') - str_list.append('%s=0s%s' % (name, encoded_val)) + str_list.append('%s=0s%s' % (C.acl_quote(name), encoded_val)) return '\n'.join(str_list)+'\n' def Record2EA(record): @@ -120,7 +119,7 @@ def Record2EA(record): raise metadata.ParsingError("Bad record beginning: " + first[:8]) filename = first[8:] if filename == '.': index = () - else: index = tuple(filename.split('/')) + else: index = tuple(C.acl_unquote(filename).split('/')) ea = ExtendedAttributes(index) for line in lines: @@ -137,27 +136,15 @@ def Record2EA(record): ea.set(name, base64.decodestring(encoded_val)) return ea -def quote_path(path): - """Quote a path for use EA/ACL records. - - Right now no quoting!!! Change this to reflect the updated - quoting style of getfattr/setfattr when they are changed. - - """ - return path - class EAExtractor(metadata.FlatExtractor): """Iterate ExtendedAttributes objects from the EA information file""" - record_boundary_regexp = re.compile("\\n# file:") + record_boundary_regexp = re.compile('(?:\\n|^)(# file: (.*?))\\n') record_to_object = staticmethod(Record2EA) - def get_index_re(self, index): - """Find start of EA record with given index""" - if not index: indexpath = '.' - else: indexpath = '/'.join(index) - # Right now there is no quoting, due to a bug in - # getfacl/setfacl. Replace later when bug fixed. - return re.compile('(^|\\n)(# file: %s\\n)' % indexpath) + def filename_to_index(self, filename): + """Convert possibly quoted filename to index tuple""" + if filename == '.': return () + else: return tuple(C.acl_unquote(filename).split('/')) class ExtendedAttributesFile(metadata.FlatFile): """Store/retrieve EAs from extended_attributes file""" @@ -171,7 +158,7 @@ class ExtendedAttributesFile(metadata.FlatFile): """Add EA information in ea_iter to rorp_iter""" collated = rorpiter.CollateIterators(rorp_iter, ea_iter) for rorp, ea in collated: - assert rorp, (rorp, (ea.index, ea.attr_dict), rest_time) + assert rorp, (rorp, (ea.index, ea.attr_dict), time) if not ea: ea = ExtendedAttributes(rorp.index) rorp.set_ea(ea) yield rorp @@ -311,7 +298,7 @@ def acl_compare_rps(rp1, rp2): def ACL2Record(acl): """Convert an AccessControlList object into a text record""" - start = "# file: %s\n%s" % (acl.get_indexpath(), acl.acl_text) + start = "# file: %s\n%s" % (C.acl_quote(acl.get_indexpath()), acl.acl_text) if not acl.def_acl_text: return start default_lines = acl.def_acl_text.strip().split('\n') default_text = '\ndefault:'.join(default_lines) @@ -325,7 +312,7 @@ def Record2ACL(record): raise metadata.ParsingError("Bad record beginning: "+ first_line) filename = first_line[8:] if filename == '.': index = () - else: index = tuple(filename.split('/')) + else: index = tuple(C.acl_unquote(filename).split('/')) normal_entries = []; default_entries = [] for line in lines: @@ -393,3 +380,25 @@ def GetCombinedMetadataIter(rbdir, time, restrict_index = None, metadata_iter, rbdir, time, restrict_index) return metadata_iter + +def rpath_acl_get(rp): + """Get acls of given rpath rp. + + This overrides a function in the rpath module. + + """ + acl = AccessControlList(rp.index) + if not rp.issym(): acl.read_from_rp(rp) + return acl +rpath.acl_get = rpath_acl_get + +def rpath_ea_get(rp): + """Get extended attributes of given rpath + + This overrides a function in the rpath module. + + """ + ea = ExtendedAttributes(rp.index) + if not rp.issym(): ea.read_from_rp(rp) + return ea +rpath.ea_get = rpath_ea_get diff --git a/rdiff-backup/rdiff_backup/metadata.py b/rdiff-backup/rdiff_backup/metadata.py index eec8e3e..2388cfa 100644 --- a/rdiff-backup/rdiff_backup/metadata.py +++ b/rdiff-backup/rdiff_backup/metadata.py @@ -122,9 +122,7 @@ def Record2RORP(record_string): """ data_dict = {} for field, data in line_parsing_regexp.findall(record_string): - if field == "File": - if data == ".": index = () - else: index = tuple(unquote_path(data).split("/")) + if field == "File": index = quoted_filename_to_index(data) elif field == "Type": if data == "None": data_dict['type'] = None else: data_dict['type'] = data @@ -174,12 +172,23 @@ def unquote_path(quoted_string): return two_chars return re.sub("\\\\n|\\\\\\\\", replacement_func, quoted_string) +def quoted_filename_to_index(quoted_filename): + """Return tuple index given quoted filename""" + if quoted_filename == '.': return () + else: return tuple(unquote_path(quoted_filename).split('/')) class FlatExtractor: """Controls iterating objects from flat file""" - # The following two should be set in subclasses - record_boundary_regexp = None # Matches beginning of next record - record_to_object = None # Function that converts text record to object + + # Set this in subclass. record_boundary_regexp should match + # beginning of next record. The first group should start at the + # beginning of the record. The second group should contain the + # (possibly quoted) filename. + record_boundary_regexp = None + + # Set in subclass to function that converts text record to object + record_to_object = None + def __init__(self, fileobj): self.fileobj = fileobj # holds file object we are reading from self.buf = "" # holds the next part of the file @@ -187,10 +196,10 @@ class FlatExtractor: self.blocksize = 32 * 1024 def get_next_pos(self): - """Return position of next record in buffer""" + """Return position of next record in buffer, or end pos if none""" while 1: - m = self.record_boundary_regexp.search(self.buf) - if m: return m.start(0)+1 # the +1 skips the newline + m = self.record_boundary_regexp.search(self.buf, 1) + if m: return m.start(1) else: # add next block to the buffer, loop again newbuf = self.fileobj.read(self.blocksize) if not newbuf: @@ -218,27 +227,20 @@ class FlatExtractor: """ assert not self.buf or self.buf.endswith("\n") - begin_re = self.get_index_re(index) while 1: - m = begin_re.search(self.buf) - if m: - self.buf = self.buf[m.start(2):] - return self.buf = self.fileobj.read(self.blocksize) self.buf += self.fileobj.readline() if not self.buf: self.at_end = 1 return - - def get_index_re(self, index): - """Return regular expression used to find index. - - Override this in sub classes. The regular expression's second - group needs to start at the beginning of the record that - contains information about the object with the given index. - - """ - assert 0, "Just a placeholder, must override this in subclasses" + while 1: + m = self.record_boundary_regexp.search(self.buf) + if not m: break + cur_index = self.filename_to_index(m.group(2)) + if cur_index >= index: + self.buf = self.buf[m.start(1):] + return + else: self.buf = self.buf[m.end(1):] def iterate_starting_with(self, index): """Iterate objects whose index starts with given index""" @@ -256,24 +258,24 @@ class FlatExtractor: self.buf = self.buf[next_pos:] assert not self.close() + def filename_to_index(self, filename): + """Translate filename, possibly quoted, into an index tuple + + The filename is the first group matched by + regexp_boundary_regexp. + + """ + assert 0 # subclass + def close(self): """Return value of closing associated file""" return self.fileobj.close() class RorpExtractor(FlatExtractor): """Iterate rorps from metadata file""" - record_boundary_regexp = re.compile("\\nFile") + record_boundary_regexp = re.compile("(?:\\n|^)(File (.*?))\\n") record_to_object = staticmethod(Record2RORP) - def get_index_re(self, index): - """Find start of rorp record with given index""" - indexpath = index and '/'.join(index) or '.' - # Must double all backslashes, because they will be - # reinterpreted. For instance, to search for index \n - # (newline), it will be \\n (backslash n) in the file, so the - # regular expression is "File \\\\n\\n" (File two backslash n - # backslash n) - double_quote = re.sub("\\\\", "\\\\\\\\", indexpath) - return re.compile("(^|\\n)(File %s\\n)" % (double_quote,)) + filename_to_index = staticmethod(quoted_filename_to_index) class FlatFile: @@ -339,7 +341,7 @@ class FlatFile: else: compressed = cls._rp.get_indexpath().endswith('.gz') fileobj = cls._rp.open('rb', compress = compressed) - if restrict_index is None: return cls._extractor(fileobj).iterate() + if not restrict_index: return cls._extractor(fileobj).iterate() else: re = cls._extractor(fileobj) return re.iterate_starting_with(restrict_index) diff --git a/rdiff-backup/rdiff_backup/rpath.py b/rdiff-backup/rdiff_backup/rpath.py index 43f14e3..c1641a5 100644 --- a/rdiff-backup/rdiff_backup/rpath.py +++ b/rdiff-backup/rdiff_backup/rpath.py @@ -1001,10 +1001,7 @@ class RPath(RORPath): def get_acl(self): """Return access control list object, setting if necessary""" try: acl = self.data['acl'] - except KeyError: - acl = eas_acls.AccessControlList(self.index) - if not self.issym(): acl.read_from_rp(self) - self.data['acl'] = acl + except KeyError: acl = self.data['acl'] = acl_get(self) return acl def write_acl(self, acl): @@ -1015,14 +1012,7 @@ class RPath(RORPath): def get_ea(self): """Return extended attributes object, setting if necessary""" try: ea = self.data['ea'] - except KeyError: - ea = eas_acls.ExtendedAttributes(self.index) - if not self.issym(): - # Don't read from symlinks because they will be - # followed. Update this when llistxattr, - # etc. available - ea.read_from_rp(self) - self.data['ea'] = ea + except KeyError: ea = self.data['ea'] = ea_get(self) return ea def write_ea(self, ea): @@ -1068,4 +1058,9 @@ class RPathFileHook: self.closing_thunk() return result -import eas_acls # Put at end to avoid regress + +# These two are overwritten by the eas_acls.py module. We can't +# import that module directory because of circular dependency +# problems. +def acl_get(rp): assert 0 +def ea_get(rp): assert 0 diff --git a/rdiff-backup/testing/ctest.py b/rdiff-backup/testing/ctest.py index 16a7882..2f11b1f 100644 --- a/rdiff-backup/testing/ctest.py +++ b/rdiff-backup/testing/ctest.py @@ -41,5 +41,13 @@ class CTest(unittest.TestCase): """Test running C.sync""" C.sync() + def test_acl_quoting(self): + """Test the acl_quote and acl_unquote functions""" + assert C.acl_quote('foo') == 'foo', C.acl_quote('foo') + assert C.acl_quote('\n') == '\\012', C.acl_quote('\n') + assert C.acl_unquote('\\012') == '\n' + s = '\\\n\t\145\n\01==' + assert C.acl_unquote(C.acl_quote(s)) == s + if __name__ == "__main__": unittest.main() diff --git a/rdiff-backup/testing/eas_aclstest.py b/rdiff-backup/testing/eas_aclstest.py index ed1a5b4..54d9f05 100644 --- a/rdiff-backup/testing/eas_aclstest.py +++ b/rdiff-backup/testing/eas_aclstest.py @@ -1,9 +1,11 @@ -import unittest, os, time +import unittest, os, time, cStringIO from commontest import * from rdiff_backup.eas_acls import * from rdiff_backup import Globals, rpath, Time tempdir = rpath.RPath(Globals.local_connection, "testfiles/output") +restore_dir = rpath.RPath(Globals.local_connection, + "testfiles/restore_out") class EATest(unittest.TestCase): """Test extended attributes""" @@ -27,6 +29,7 @@ class EATest(unittest.TestCase): """Make temp directory testfiles/output""" if tempdir.lstat(): tempdir.delete() tempdir.mkdir() + if restore_dir.lstat(): restore_dir.delete() def testBasic(self): """Test basic writing and reading of extended attributes""" @@ -61,6 +64,41 @@ class EATest(unittest.TestCase): (self.sample_ea.index, new_ea.index) assert 0, "We shouldn't have gotten this far" + def testExtractor(self): + """Test seeking inside a record list""" + record_list = """# file: 0foo +user.multiline=0sVGhpcyBpcyBhIGZhaXJseSBsb25nIGV4dGVuZGVkIGF0dHJpYnV0ZS4KCQkJIEVuY29kaW5nIGl0IHdpbGwgcmVxdWlyZSBzZXZlcmFsIGxpbmVzIG9mCgkJCSBiYXNlNjQusbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx +user.third=0saGVsbG8= +user.not_empty=0sZm9vYmFy +user.binary=0sAAECjC89Ig== +user.empty +# file: 1foo/bar/baz +user.multiline=0sVGhpcyBpcyBhIGZhaXJseSBsb25nIGV4dGVuZGVkIGF0dHJpYnV0ZS4KCQkJIEVuY29kaW5nIGl0IHdpbGwgcmVxdWlyZSBzZXZlcmFsIGxpbmVzIG9mCgkJCSBiYXNlNjQusbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx +user.third=0saGVsbG8= +user.binary=0sAAECjC89Ig== +user.empty +# file: 2foo/\\012 +user.empty +""" + extractor = EAExtractor(cStringIO.StringIO(record_list)) + ea_iter = extractor.iterate_starting_with(()) + first = ea_iter.next() + assert first.index == ('0foo',), first + second = ea_iter.next() + assert second.index == ('1foo', 'bar', 'baz'), second + third = ea_iter.next() # Test quoted filenames + assert third.index == ('2foo', '\n'), third.index + try: ea_iter.next() + except StopIteration: pass + else: assert 0, "Too many elements in iterator" + + extractor = EAExtractor(cStringIO.StringIO(record_list)) + ea_iter = extractor.iterate_starting_with(('1foo', 'bar')) + assert ea_iter.next().index == ('1foo', 'bar', 'baz') + try: ea_iter.next() + except StopIteration: pass + else: assert 0, "Too many elements in iterator" + def make_backup_dirs(self): """Create testfiles/ea_test[12] directories @@ -137,6 +175,21 @@ class EATest(unittest.TestCase): 'testfiles/empty', 'testfiles/ea_test1'] BackupRestoreSeries(None, None, dirlist, compare_eas = 1) + def test_final_local(self): + """Test backing up and restoring using 'rdiff-backup' script""" + self.make_backup_dirs() + self.make_temp() + rdiff_backup(1, 1, self.ea_testdir1.path, tempdir.path, + current_time = 10000) + assert CompareRecursive(self.ea_testdir1, tempdir, compare_eas = 1) + + rdiff_backup(1, 1, self.ea_testdir2.path, tempdir.path, + current_time = 20000) + assert CompareRecursive(self.ea_testdir2, tempdir, compare_eas = 1) + + rdiff_backup(1, 1, tempdir.path, restore_dir.path, + extra_options = '-r 10000') + assert CompareRecursive(self.ea_testdir1, restore_dir, compare_eas = 1) class ACLTest(unittest.TestCase): @@ -181,6 +234,7 @@ other::---""") """Make temp directory testfile/output""" if tempdir.lstat(): tempdir.delete() tempdir.mkdir() + if restore_dir.lstat(): restore_dir.delete() def testBasic(self): """Test basic writing and reading of ACLs""" @@ -227,6 +281,49 @@ other::---""") assert new_acl2.eq_verbose(self.dir_acl) assert 0 + def testExtractor(self): + """Test seeking inside a record list""" + record_list = """# file: 0foo +user::r-- +user:ben:--- +group::--- +group:root:--- +mask::--- +other::--- +# file: 1foo/bar/baz +user::r-- +user:ben:--- +group::--- +group:root:--- +mask::--- +other::--- +# file: 2foo/\\012 +user::r-- +user:ben:--- +group::--- +group:root:--- +mask::--- +other::--- +""" + extractor = ACLExtractor(cStringIO.StringIO(record_list)) + acl_iter = extractor.iterate_starting_with(()) + first = acl_iter.next() + assert first.index == ('0foo',), first + second = acl_iter.next() + assert second.index == ('1foo', 'bar', 'baz'), second + third = acl_iter.next() # Test quoted filenames + assert third.index == ('2foo', '\n'), third.index + try: acl_iter.next() + except StopIteration: pass + else: assert 0, "Too many elements in iterator" + + extractor = ACLExtractor(cStringIO.StringIO(record_list)) + acl_iter = extractor.iterate_starting_with(('1foo', 'bar')) + assert acl_iter.next().index == ('1foo', 'bar', 'baz') + try: acl_iter.next() + except StopIteration: pass + else: assert 0, "Too many elements in iterator" + def make_backup_dirs(self): """Create testfiles/acl_test[12] directories""" if self.acl_testdir1.lstat(): self.acl_testdir1.delete() @@ -295,6 +392,29 @@ other::---""") 'testfiles/empty', 'testfiles/acl_test1'] BackupRestoreSeries(None, None, dirlist, compare_acls = 1) + def test_final_local(self): + """Test backing up and restoring using 'rdiff-backup' script""" + self.make_backup_dirs() + self.make_temp() + rdiff_backup(1, 1, self.acl_testdir1.path, tempdir.path, + current_time = 10000) + assert CompareRecursive(self.acl_testdir1, tempdir, compare_acls = 1) + + rdiff_backup(1, 1, self.acl_testdir2.path, tempdir.path, + current_time = 20000) + assert CompareRecursive(self.acl_testdir2, tempdir, compare_acls = 1) + + rdiff_backup(1, 1, tempdir.path, restore_dir.path, + extra_options = '-r 10000') + assert CompareRecursive(self.acl_testdir1, restore_dir, + compare_acls = 1) + + restore_dir.delete() + rdiff_backup(1, 1, tempdir.path, restore_dir.path, + extra_options = '-r now') + assert CompareRecursive(self.acl_testdir2, restore_dir, + compare_acls = 1) + class CombinedTest(unittest.TestCase): """Test backing up and restoring directories with both EAs and ACLs""" -- cgit v1.2.1