summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rdiff-backup/dist/rdiff-backup.spec51
-rw-r--r--rdiff-backup/rdiff_backup/eas_acls.py395
-rw-r--r--rdiff-backup/testing/eas_aclstest.py344
-rw-r--r--rdiff-backup/testing/resourceforktest.py104
-rw-r--r--rdiff-backup/testing/securitytest.py128
5 files changed, 1019 insertions, 3 deletions
diff --git a/rdiff-backup/dist/rdiff-backup.spec b/rdiff-backup/dist/rdiff-backup.spec
new file mode 100644
index 0000000..2073b51
--- /dev/null
+++ b/rdiff-backup/dist/rdiff-backup.spec
@@ -0,0 +1,51 @@
+%define PYTHON_NAME %((rpm -q --quiet python2 && echo python2) || echo python)
+
+Version: $version
+Summary: Convenient and transparent local/remote incremental mirror/backup
+Name: rdiff-backup
+Release: 2
+URL: http://www.stanford.edu/~bescoto/rdiff-backup/
+Source: %{name}-%{version}.tar.gz
+Copyright: GPL
+Group: Applications/Archiving
+BuildRoot: %{_tmppath}/%{name}-root
+requires: librsync >= 0.9.5.1, %{PYTHON_NAME} >= 2.2
+BuildPrereq: %{PYTHON_NAME}-devel >= 2.2, librsync-devel >= 0.9.5.1
+
+%description
+rdiff-backup is a script, written in Python, that backs up one
+directory to another and is intended to be run periodically (nightly
+from cron for instance). The target directory ends up a copy of the
+source directory, but extra reverse diffs are stored in the target
+directory, so you can still recover files lost some time ago. The idea
+is to combine the best features of a mirror and an incremental
+backup. rdiff-backup can also operate in a bandwidth efficient manner
+over a pipe, like rsync. Thus you can use rdiff-backup and ssh to
+securely back a hard drive up to a remote location, and only the
+differences from the previous backup will be transmitted.
+
+%prep
+%setup -q
+
+%build
+%{PYTHON_NAME} setup.py build
+
+%install
+%{PYTHON_NAME} setup.py install --prefix=$RPM_BUILD_ROOT/usr
+
+%clean
+[ "$RPM_BUILD_ROOT" != "/" ] && rm -rf $RPM_BUILD_ROOT
+
+%files
+%defattr(-,root,root)
+/usr/bin/rdiff-backup
+/usr/share/man/man1
+/usr/lib
+%doc CHANGELOG COPYING FAQ.html README
+
+%changelog
+* Sun Jan 19 2002 Troels Arvin <troels@arvin.dk>
+- Builds, no matter if Python 2.2 is called python2-2.2 or python-2.2.
+
+* Sun Nov 4 2001 Ben Escoto <bescoto@stanford.edu>
+- Initial RPM
diff --git a/rdiff-backup/rdiff_backup/eas_acls.py b/rdiff-backup/rdiff_backup/eas_acls.py
new file mode 100644
index 0000000..4b4d169
--- /dev/null
+++ b/rdiff-backup/rdiff_backup/eas_acls.py
@@ -0,0 +1,395 @@
+# Copyright 2003 Ben Escoto
+#
+# This file is part of rdiff-backup.
+#
+# rdiff-backup is free software; you can redistribute it and/or modify
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# rdiff-backup is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with rdiff-backup; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+# USA
+
+"""Store and retrieve extended attributes and access control lists
+
+Not all file systems will have EAs and ACLs, but if they do, store
+this information in separate files in the rdiff-backup-data directory,
+called extended_attributes.<time>.snapshot and
+access_control_lists.<time>.snapshot.
+
+"""
+
+from __future__ import generators
+import base64, errno, re
+try: import posix1e
+except ImportError: pass
+import static, Globals, metadata, connection, rorpiter, log
+
+
+class ExtendedAttributes:
+ """Hold a file's extended attribute information"""
+ def __init__(self, index, attr_dict = None):
+ """Initialize EA object with no attributes"""
+ self.index = index
+ if attr_dict is None: self.attr_dict = {}
+ else: self.attr_dict = attr_dict
+
+ def __eq__(self, ea):
+ """Equal if all attributes and index are equal"""
+ assert isinstance(ea, ExtendedAttributes)
+ return ea.index == self.index and ea.attr_dict == self.attr_dict
+ def __ne__(self, ea): return not self.__eq__(ea)
+
+ def get_indexpath(self): return self.index and '/'.join(self.index) or '.'
+
+ def read_from_rp(self, rp):
+ """Set the extended attributes from an rpath"""
+ try: attr_list = rp.conn.xattr.listxattr(rp.path)
+ except IOError, exc:
+ if exc[0] == errno.EOPNOTSUPP: return # if not sup, consider empty
+ raise
+ for attr in attr_list:
+ if not attr.startswith('user.'):
+ # Only preserve user extended attributes
+ continue
+ try: self.attr_dict[attr] = rp.conn.xattr.getxattr(rp.path, attr)
+ except IOError, exc:
+ # File probably modified while reading, just continue
+ if exc[0] == errno.ENODATA: continue
+ elif exc[0] == errno.ENOENT: break
+ else: raise
+
+ def clear_rp(self, rp):
+ """Delete all the extended attributes in rpath"""
+ for name in rp.conn.xattr.listxattr(rp.path):
+ rp.conn.xattr.removexattr(rp.path, name)
+
+ def write_to_rp(self, rp):
+ """Write extended attributes to rpath rp"""
+ self.clear_rp(rp)
+ for (name, value) in self.attr_dict.iteritems():
+ rp.conn.xattr.setxattr(rp.path, name, value)
+
+ def get(self, name):
+ """Return attribute attached to given name"""
+ return self.attr_dict[name]
+
+ def set(self, name, value = ""):
+ """Set given name to given value. Does not write to disk"""
+ self.attr_dict[name] = value
+
+ def delete(self, name):
+ """Delete value associated with given name"""
+ del self.attr_dict[name]
+
+ def empty(self):
+ """Return true if no extended attributes are set"""
+ return not self.attr_dict
+
+def ea_compare_rps(rp1, rp2):
+ """Return true if rp1 and rp2 have same extended attributes"""
+ ea1 = ExtendedAttributes(rp1.index)
+ ea1.read_from_rp(rp1)
+ ea2 = ExtendedAttributes(rp2.index)
+ ea2.read_from_rp(rp2)
+ return ea1 == ea2
+
+
+def EA2Record(ea):
+ """Convert ExtendedAttributes object to text record"""
+ str_list = ['# file: %s' % 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))
+ return '\n'.join(str_list)+'\n'
+
+def Record2EA(record):
+ """Convert text record to ExtendedAttributes object"""
+ lines = record.split('\n')
+ first = lines.pop(0)
+ if not first[:8] == "# file: ":
+ raise metadata.ParsingError("Bad record beginning: " + first[:8])
+ filename = first[8:]
+ if filename == '.': index = ()
+ else: index = tuple(filename.split('/'))
+ ea = ExtendedAttributes(index)
+
+ for line in lines:
+ line = line.strip()
+ if not line: continue
+ assert line[0] != '#', line
+ eq_pos = line.find('=')
+ if eq_pos == -1: ea.set(line)
+ else:
+ name = line[:eq_pos]
+ assert line[eq_pos+1:eq_pos+3] == '0s', \
+ "Currently only base64 encoding supported"
+ encoded_val = line[eq_pos+3:]
+ 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_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)
+
+class ExtendedAttributesFile(metadata.FlatFile):
+ """Store/retrieve EAs from extended_attributes file"""
+ _prefix = "extended_attributes"
+ _extractor = EAExtractor
+ _object_to_record = staticmethod(EA2Record)
+
+ def join(cls, rorp_iter, rbdir, time, restrict_index):
+ """Add extended attribute information to existing rorp_iter"""
+ def helper(rorp_iter, ea_iter):
+ """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)
+ if not ea: ea = ExtendedAttributes(rorp.index)
+ rorp.set_ea(ea)
+ yield rorp
+
+ ea_iter = cls.get_objects_at_time(rbdir, time, restrict_index)
+ if ea_iter: return helper(rorp_iter, ea_iter)
+ else:
+ log.Log("Warning: Extended attributes file not found",2)
+ return rorp_iter
+
+static.MakeClass(ExtendedAttributesFile)
+
+
+class AccessControlList:
+ """Hold a file's access control list information
+
+ Since ACL objects cannot be picked, store everything as text, in
+ self.acl_text and self.def_acl_text.
+
+ """
+ def __init__(self, index, acl_text = None, def_acl_text = None):
+ """Initialize object with index and possibly acl_text"""
+ self.index = index
+ if acl_text: # Check validity of ACL, reorder if necessary
+ ACL = posix1e.ACL(text=acl_text)
+ assert ACL.valid(), "Bad ACL: "+acl_text
+ self.acl_text = str(ACL)
+ else: self.acl_text = None
+
+ if def_acl_text:
+ def_ACL = posix1e.ACL(text=def_acl_text)
+ assert def_ACL.valid(), "Bad default ACL: "+def_acl_text
+ self.def_acl_text = str(def_ACL)
+ else: self.def_acl_text = None
+
+ def __str__(self):
+ """Return human-readable string"""
+ return ("acl_text: %s\ndef_acl_text: %s" %
+ (self.acl_text, self.def_acl_text))
+
+ def __eq__(self, acl):
+ """Compare self and other access control list
+
+ Basic acl permissions are considered equal to an empty acl
+ object.
+
+ """
+ assert isinstance(acl, self.__class__)
+ if self.index != acl.index: return 0
+ if self.is_basic(): return acl.is_basic()
+ if acl.is_basic(): return self.is_basic()
+ if self.acl_text != acl.acl_text: return 0
+ if not self.def_acl_text and not acl.def_acl_text: return 1
+ return self.def_acl_text == acl.def_acl_text
+ def __ne__(self, acl): return not self.__eq__(acl)
+
+ def eq_verbose(self, acl):
+ """Returns same as __eq__ but print explanation if not equal"""
+ if self.index != acl.index:
+ print "index %s not equal to index %s" % (self.index, acl.index)
+ return 0
+ if self.acl_text != acl.acl_text:
+ print "ACL texts not equal:"
+ print self.acl_text
+ print acl.acl_text
+ return 0
+ if (self.def_acl_text != acl.def_acl_text and
+ (self.def_acl_text or acl.def_acl_text)):
+ print "Unequal default acl texts:"
+ print self.def_acl_text
+ print acl.def_acl_text
+ return 0
+ return 1
+
+ def get_indexpath(self): return self.index and '/'.join(self.index) or '.'
+
+ def is_basic(self):
+ """True if acl can be reduced to standard unix permissions
+
+ Assume that if they are only three entries, they correspond to
+ user, group, and other, and thus don't use any special ACL
+ features.
+
+ """
+ if not self.acl_text and not self.def_acl_text: return 1
+ lines = self.acl_text.strip().split('\n')
+ assert len(lines) >= 3, lines
+ return len(lines) == 3 and not self.def_acl_text
+
+ def read_from_rp(self, rp):
+ """Set self.ACL from an rpath, or None if not supported"""
+ self.acl_text, self.def_acl_text = \
+ rp.conn.eas_acls.get_acl_text_from_rp(rp)
+
+ def write_to_rp(self, rp):
+ """Write current access control list to RPath rp"""
+ rp.conn.eas_acls.set_rp_acl(rp, self.acl_text, self.def_acl_text)
+
+def set_rp_acl(rp, acl_text = None, def_acl_text = None):
+ """Set given rp with ACL that acl_text defines. rp should be local"""
+ assert rp.conn is Globals.local_connection
+ if acl_text:
+ acl = posix1e.ACL(text=acl_text)
+ assert acl.valid()
+ else: acl = posix1e.ACL()
+ acl.applyto(rp.path)
+ if rp.isdir():
+ if def_acl_text:
+ def_acl = posix1e.ACL(text=def_acl_text)
+ assert def_acl.valid()
+ else: def_acl = posix1e.ACL()
+ def_acl.applyto(rp.path, posix1e.ACL_TYPE_DEFAULT)
+
+def get_acl_text_from_rp(rp):
+ """Returns (acl_text, def_acl_text) from an rpath. Call locally"""
+ assert rp.conn is Globals.local_connection
+ try: acl_text = str(posix1e.ACL(file=rp.path))
+ except IOError, exc:
+ if exc[0] == errno.EOPNOTSUPP: acl_text = None
+ else: raise
+ if rp.isdir():
+ try: def_acl_text = str(posix1e.ACL(filedef=rp.path))
+ except IOError, exc:
+ if exc[0] == errno.EOPNOTSUPP: def_acl_text = None
+ else: raise
+ else: def_acl_text = None
+ return (acl_text, def_acl_text)
+
+def acl_compare_rps(rp1, rp2):
+ """Return true if rp1 and rp2 have same acl information"""
+ acl1 = AccessControlList(rp1.index)
+ acl1.read_from_rp(rp1)
+ acl2 = AccessControlList(rp2.index)
+ acl2.read_from_rp(rp2)
+ return acl1 == acl2
+
+
+def ACL2Record(acl):
+ """Convert an AccessControlList object into a text record"""
+ start = "# file: %s\n%s" % (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)
+ return "%sdefault:%s\n" % (start, default_text)
+
+def Record2ACL(record):
+ """Convert text record to an AccessControlList object"""
+ lines = record.split('\n')
+ first_line = lines.pop(0)
+ if not first_line.startswith('# file: '):
+ raise metadata.ParsingError("Bad record beginning: "+ first_line)
+ filename = first_line[8:]
+ if filename == '.': index = ()
+ else: index = tuple(filename.split('/'))
+
+ normal_entries = []; default_entries = []
+ for line in lines:
+ if line.startswith('default:'): default_entries.append(line[8:])
+ else: normal_entries.append(line)
+ return AccessControlList(index, acl_text='\n'.join(normal_entries),
+ def_acl_text='\n'.join(default_entries))
+
+
+class ACLExtractor(EAExtractor):
+ """Iterate AccessControlList objects from the ACL information file
+
+ Except for the record_to_object method, we can reuse everything in
+ the EAExtractor class because the file formats are so similar.
+
+ """
+ record_to_object = staticmethod(Record2ACL)
+
+class AccessControlListFile(metadata.FlatFile):
+ """Store/retrieve ACLs from extended attributes file"""
+ _prefix = 'access_control_lists'
+ _extractor = ACLExtractor
+ _object_to_record = staticmethod(ACL2Record)
+
+ def join(cls, rorp_iter, rbdir, time, restrict_index):
+ """Add access control list information to existing rorp_iter"""
+ def helper(rorp_iter, acl_iter):
+ """Add ACL information in acl_iter to rorp_iter"""
+ collated = rorpiter.CollateIterators(rorp_iter, acl_iter)
+ for rorp, acl in collated:
+ assert rorp, "Missing rorp for index %s" % (acl.index,)
+ if not acl: acl = AccessControlList(rorp.index)
+ rorp.set_acl(acl)
+ yield rorp
+
+ acl_iter = cls.get_objects_at_time(rbdir, time, restrict_index)
+ if acl_iter: return helper(rorp_iter, acl_iter)
+ else:
+ log.Log("Warning: Access Control List file not found", 2)
+ return rorp_iter
+
+static.MakeClass(AccessControlListFile)
+
+
+def GetCombinedMetadataIter(rbdir, time, restrict_index = None,
+ acls = None, eas = None):
+ """Return iterator of rorps from metadata and related files
+
+ None will be returned if the metadata file is absent. If acls or
+ eas is true, access control list or extended attribute information
+ will be added.
+
+ """
+ metadata_iter = metadata.MetadataFile.get_objects_at_time(
+ rbdir, time, restrict_index)
+ if not metadata_iter:
+ log.Log("Warning, metadata file not found.\n"
+ "Metadata will be read from filesystem.", 2)
+ return None
+ if eas:
+ metadata_iter = ExtendedAttributesFile.join(
+ metadata_iter, rbdir, time, restrict_index)
+ if acls:
+ metadata_iter = AccessControlListFile.join(
+ metadata_iter, rbdir, time, restrict_index)
+ return metadata_iter
+
diff --git a/rdiff-backup/testing/eas_aclstest.py b/rdiff-backup/testing/eas_aclstest.py
new file mode 100644
index 0000000..ed1a5b4
--- /dev/null
+++ b/rdiff-backup/testing/eas_aclstest.py
@@ -0,0 +1,344 @@
+import unittest, os, time
+from commontest import *
+from rdiff_backup.eas_acls import *
+from rdiff_backup import Globals, rpath, Time
+
+tempdir = rpath.RPath(Globals.local_connection, "testfiles/output")
+
+class EATest(unittest.TestCase):
+ """Test extended attributes"""
+ sample_ea = ExtendedAttributes(
+ (), {'user.empty':'', 'user.not_empty':'foobar', 'user.third':'hello',
+ 'user.binary':chr(0)+chr(1)+chr(2)+chr(140)+'/="',
+ 'user.multiline':"""This is a fairly long extended attribute.
+ Encoding it will require several lines of
+ base64.""" + chr(177)*300})
+ empty_ea = ExtendedAttributes(())
+ ea1 = ExtendedAttributes(('1',), sample_ea.attr_dict.copy())
+ ea1.delete('user.not_empty')
+ ea2 = ExtendedAttributes(('2',), sample_ea.attr_dict.copy())
+ ea2.set('user.third', 'Another random attribute')
+ ea3 = ExtendedAttributes(('3',))
+ ea4 = ExtendedAttributes(('4',), {'user.deleted': 'File to be deleted'})
+ ea_testdir1 = rpath.RPath(Globals.local_connection, "testfiles/ea_test1")
+ ea_testdir2 = rpath.RPath(Globals.local_connection, "testfiles/ea_test2")
+
+ def make_temp(self):
+ """Make temp directory testfiles/output"""
+ if tempdir.lstat(): tempdir.delete()
+ tempdir.mkdir()
+
+ def testBasic(self):
+ """Test basic writing and reading of extended attributes"""
+ self.make_temp()
+ new_ea = ExtendedAttributes(())
+ new_ea.read_from_rp(tempdir)
+ assert not new_ea.attr_dict
+ assert not new_ea == self.sample_ea
+ assert new_ea != self.sample_ea
+ assert new_ea == self.empty_ea
+
+ self.sample_ea.write_to_rp(tempdir)
+ new_ea.read_from_rp(tempdir)
+ assert new_ea.attr_dict == self.sample_ea.attr_dict, \
+ (new_ea.attr_dict, self.sample_ea.attr_dict)
+ assert new_ea == self.sample_ea
+
+ def testRecord(self):
+ """Test writing a record and reading it back"""
+ record = EA2Record(self.sample_ea)
+ new_ea = Record2EA(record)
+ if not new_ea == self.sample_ea:
+ new_list = new_ea.attr_dict.keys()
+ sample_list = self.sample_ea.attr_dict.keys()
+ new_list.sort()
+ sample_list.sort()
+ assert new_list == sample_list, (new_list, sample_list)
+ for name in new_list:
+ assert self.sample_ea.get(name) == new_ea.get(name), \
+ (self.sample_ea.get(name), new_ea.get(name))
+ assert self.sample_ea.index == new_ea.index, \
+ (self.sample_ea.index, new_ea.index)
+ assert 0, "We shouldn't have gotten this far"
+
+ def make_backup_dirs(self):
+ """Create testfiles/ea_test[12] directories
+
+ Goal is to set range of extended attributes, to give good test
+ to extended attribute code.
+
+ """
+ if self.ea_testdir1.lstat(): self.ea_testdir1.delete()
+ if self.ea_testdir2.lstat(): self.ea_testdir2.delete()
+ self.ea_testdir1.mkdir()
+ rp1_1 = self.ea_testdir1.append('1')
+ rp1_2 = self.ea_testdir1.append('2')
+ rp1_3 = self.ea_testdir1.append('3')
+ rp1_4 = self.ea_testdir1.append('4')
+ map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3, rp1_4])
+ self.sample_ea.write_to_rp(self.ea_testdir1)
+ self.ea1.write_to_rp(rp1_1)
+ self.ea2.write_to_rp(rp1_2)
+ self.ea4.write_to_rp(rp1_4)
+
+ self.ea_testdir2.mkdir()
+ rp2_1 = self.ea_testdir2.append('1')
+ rp2_2 = self.ea_testdir2.append('2')
+ rp2_3 = self.ea_testdir2.append('3')
+ map(rpath.RPath.touch, [rp2_1, rp2_2, rp2_3])
+ self.ea3.write_to_rp(self.ea_testdir2)
+ self.sample_ea.write_to_rp(rp2_1)
+ self.ea1.write_to_rp(rp2_2)
+ self.ea2.write_to_rp(rp2_3)
+
+ def testIterate(self):
+ """Test writing several records and then reading them back"""
+ self.make_backup_dirs()
+ rp1 = self.ea_testdir1.append('1')
+ rp2 = self.ea_testdir1.append('2')
+ rp3 = self.ea_testdir1.append('3')
+
+ # Now write records corresponding to above rps into file
+ Globals.rbdir = tempdir
+ Time.setcurtime(10000)
+ ExtendedAttributesFile.open_file()
+ for rp in [self.ea_testdir1, rp1, rp2, rp3]:
+ ea = ExtendedAttributes(rp.index)
+ ea.read_from_rp(rp)
+ ExtendedAttributesFile.write_object(ea)
+ ExtendedAttributesFile.close_file()
+
+ # Read back records and compare
+ ea_iter = ExtendedAttributesFile.get_objects_at_time(tempdir, 10000)
+ assert ea_iter, "No extended_attributes.<time> file found"
+ sample_ea_reread = ea_iter.next()
+ assert sample_ea_reread == self.sample_ea
+ ea1_reread = ea_iter.next()
+ assert ea1_reread == self.ea1
+ ea2_reread = ea_iter.next()
+ assert ea2_reread == self.ea2
+ ea3_reread = ea_iter.next()
+ assert ea3_reread == self.ea3
+ try: ea_iter.next()
+ except StopIteration: pass
+ else: assert 0, "Expected end to iterator"
+
+ def testSeriesLocal(self):
+ """Test backing up and restoring directories with EAs locally"""
+ self.make_backup_dirs()
+ dirlist = ['testfiles/ea_test1', 'testfiles/empty',
+ 'testfiles/ea_test2', 'testfiles/ea_test1']
+ BackupRestoreSeries(1, 1, dirlist, compare_eas = 1)
+
+ def testSeriesRemote(self):
+ """Test backing up, restoring directories with EA remotely"""
+ self.make_backup_dirs()
+ dirlist = ['testfiles/ea_test1', 'testfiles/ea_test2',
+ 'testfiles/empty', 'testfiles/ea_test1']
+ BackupRestoreSeries(None, None, dirlist, compare_eas = 1)
+
+
+
+class ACLTest(unittest.TestCase):
+ """Test access control lists"""
+ sample_acl = AccessControlList((),"""user::rwx
+user:root:rwx
+group::r-x
+group:root:r-x
+mask::r-x
+other::---""")
+ dir_acl = AccessControlList((), """user::rwx
+user:root:rwx
+group::r-x
+group:root:r-x
+mask::r-x
+other::---""",
+ """user::rwx
+user:root:---
+group::r-x
+mask::r-x
+other::---""")
+ acl1 = AccessControlList(('1',), """user::r--
+user:ben:---
+group::---
+group:root:---
+mask::---
+other::---""")
+ acl2 = AccessControlList(('2',), """user::rwx
+group::r-x
+group:ben:rwx
+mask::---
+other::---""")
+ acl3 = AccessControlList(('3',), """user::rwx
+user:root:---
+group::r-x
+mask::---
+other::---""")
+ empty_acl = AccessControlList((), "user::rwx\ngroup::---\nother::---")
+ acl_testdir1 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test1')
+ acl_testdir2 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test2')
+ def make_temp(self):
+ """Make temp directory testfile/output"""
+ if tempdir.lstat(): tempdir.delete()
+ tempdir.mkdir()
+
+ def testBasic(self):
+ """Test basic writing and reading of ACLs"""
+ self.make_temp()
+ new_acl = AccessControlList(())
+ tempdir.chmod(0700)
+ new_acl.read_from_rp(tempdir)
+ assert new_acl.is_basic(), new_acl.acl_text
+ assert not new_acl == self.sample_acl
+ assert new_acl != self.sample_acl
+ assert new_acl == self.empty_acl, \
+ (new_acl.acl_text, self.empty_acl.acl_text)
+
+ self.sample_acl.write_to_rp(tempdir)
+ new_acl.read_from_rp(tempdir)
+ assert new_acl.acl_text == self.sample_acl.acl_text, \
+ (new_acl.acl_text, self.sample_acl.acl_text)
+ assert new_acl == self.sample_acl
+
+ def testBasicDir(self):
+ """Test reading and writing of ACL w/ defaults to directory"""
+ self.make_temp()
+ new_acl = AccessControlList(())
+ new_acl.read_from_rp(tempdir)
+ assert new_acl.is_basic()
+ assert new_acl != self.dir_acl
+
+ self.dir_acl.write_to_rp(tempdir)
+ new_acl.read_from_rp(tempdir)
+ assert not new_acl.is_basic()
+ if not new_acl == self.dir_acl:
+ assert new_acl.eq_verbose(self.dir_acl)
+ assert 0, "Shouldn't be here---eq != eq_verbose?"
+
+ def testRecord(self):
+ """Test writing a record and reading it back"""
+ record = ACL2Record(self.sample_acl)
+ new_acl = Record2ACL(record)
+ assert new_acl == self.sample_acl
+
+ record2 = ACL2Record(self.dir_acl)
+ new_acl2 = Record2ACL(record2)
+ if not new_acl2 == self.dir_acl:
+ assert new_acl2.eq_verbose(self.dir_acl)
+ assert 0
+
+ def make_backup_dirs(self):
+ """Create testfiles/acl_test[12] directories"""
+ if self.acl_testdir1.lstat(): self.acl_testdir1.delete()
+ if self.acl_testdir2.lstat(): self.acl_testdir2.delete()
+ self.acl_testdir1.mkdir()
+ rp1_1 = self.acl_testdir1.append('1')
+ rp1_2 = self.acl_testdir1.append('2')
+ rp1_3 = self.acl_testdir1.append('3')
+ map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3])
+ self.dir_acl.write_to_rp(self.acl_testdir1)
+ self.acl1.write_to_rp(rp1_1)
+ self.acl2.write_to_rp(rp1_2)
+ self.acl3.write_to_rp(rp1_3)
+
+ self.acl_testdir2.mkdir()
+ rp2_1, rp2_2, rp2_3 = map(self.acl_testdir2.append, ('1', '2', '3'))
+ map(rpath.RPath.touch, (rp2_1, rp2_2, rp2_3))
+ self.sample_acl.write_to_rp(self.acl_testdir2)
+ self.acl3.write_to_rp(rp2_1)
+ self.acl1.write_to_rp(rp2_2)
+ self.acl2.write_to_rp(rp2_3)
+
+ def testIterate(self):
+ """Test writing several records and then reading them back"""
+ self.make_backup_dirs()
+ rp1 = self.acl_testdir1.append('1')
+ rp2 = self.acl_testdir1.append('2')
+ rp3 = self.acl_testdir1.append('3')
+
+ # Now write records corresponding to above rps into file
+ Globals.rbdir = tempdir
+ Time.setcurtime(10000)
+ AccessControlListFile.open_file()
+ for rp in [self.acl_testdir1, rp1, rp2, rp3]:
+ acl = AccessControlList(rp.index)
+ acl.read_from_rp(rp)
+ AccessControlListFile.write_object(acl)
+ AccessControlListFile.close_file()
+
+ # Read back records and compare
+ acl_iter = AccessControlListFile.get_objects_at_time(tempdir, 10000)
+ assert acl_iter, "No acl file found"
+ dir_acl_reread = acl_iter.next()
+ assert dir_acl_reread == self.dir_acl
+ acl1_reread = acl_iter.next()
+ assert acl1_reread == self.acl1
+ acl2_reread = acl_iter.next()
+ assert acl2_reread == self.acl2
+ acl3_reread = acl_iter.next()
+ assert acl3_reread == self.acl3
+ try: extra = acl_iter.next()
+ except StopIteration: pass
+ else: assert 0, "Got unexpected object: " + repr(extra)
+
+ def testSeriesLocal(self):
+ """Test backing up and restoring directories with ACLs locally"""
+ self.make_backup_dirs()
+ dirlist = ['testfiles/acl_test1', 'testfiles/empty',
+ 'testfiles/acl_test2', 'testfiles/acl_test1']
+ BackupRestoreSeries(1, 1, dirlist, compare_acls = 1)
+
+ def testSeriesRemote(self):
+ """Test backing up, restoring directories with EA remotely"""
+ self.make_backup_dirs()
+ dirlist = ['testfiles/acl_test1', 'testfiles/acl_test2',
+ 'testfiles/empty', 'testfiles/acl_test1']
+ BackupRestoreSeries(None, None, dirlist, compare_acls = 1)
+
+
+class CombinedTest(unittest.TestCase):
+ """Test backing up and restoring directories with both EAs and ACLs"""
+ combo_testdir1 = rpath.RPath(Globals.local_connection,
+ 'testfiles/ea_acl_test1')
+ combo_testdir2 = rpath.RPath(Globals.local_connection,
+ 'testfiles/ea_acl_test2')
+ def make_backup_dirs(self):
+ """Create testfiles/ea_acl_test[12] directories"""
+ if self.combo_testdir1.lstat(): self.combo_testdir1.delete()
+ if self.combo_testdir2.lstat(): self.combo_testdir2.delete()
+ self.combo_testdir1.mkdir()
+ rp1_1, rp1_2, rp1_3 = map(self.combo_testdir1.append, ('1', '2', '3'))
+ map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3])
+ ACLTest.dir_acl.write_to_rp(self.combo_testdir1)
+ EATest.sample_ea.write_to_rp(self.combo_testdir1)
+ ACLTest.acl1.write_to_rp(rp1_1)
+ EATest.ea2.write_to_rp(rp1_2)
+ ACLTest.acl3.write_to_rp(rp1_3)
+ EATest.ea3.write_to_rp(rp1_3)
+
+ self.combo_testdir2.mkdir()
+ rp2_1, rp2_2, rp2_3 = map(self.combo_testdir2.append, ('1', '2', '3'))
+ map(rpath.RPath.touch, [rp2_1, rp2_2, rp2_3])
+ ACLTest.sample_acl.write_to_rp(self.combo_testdir2)
+ EATest.ea1.write_to_rp(rp2_1)
+ EATest.ea3.write_to_rp(rp2_2)
+ ACLTest.acl2.write_to_rp(rp2_2)
+
+ def testSeriesLocal(self):
+ """Test backing up and restoring EAs/ACLs locally"""
+ self.make_backup_dirs()
+ dirlist = ['testfiles/ea_acl_test1', 'testfiles/ea_acl_test2',
+ 'testfiles/empty', 'testfiles/ea_acl_test1']
+ BackupRestoreSeries(1, 1, dirlist,
+ compare_eas = 1, compare_acls = 1)
+
+ def testSeriesRemote(self):
+ """Test backing up and restoring EAs/ACLs locally"""
+ self.make_backup_dirs()
+ dirlist = ['testfiles/ea_acl_test1', 'testfiles/empty',
+ 'testfiles/ea_acl_test2', 'testfiles/ea_acl_test1']
+ BackupRestoreSeries(None, None, dirlist,
+ compare_eas = 1, compare_acls = 1)
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/rdiff-backup/testing/resourceforktest.py b/rdiff-backup/testing/resourceforktest.py
new file mode 100644
index 0000000..4617b28
--- /dev/null
+++ b/rdiff-backup/testing/resourceforktest.py
@@ -0,0 +1,104 @@
+import unittest
+from commontest import *
+from rdiff_backup import rpath
+from rdiff_backup import metadata
+
+"""***NOTE***
+
+None of these tests should work unless your system supports resource
+forks. So basically these tests should only be run on Mac OS X.
+
+"""
+
+Globals.read_resource_forks = Globals.write_resource_forks = 1
+
+class ResourceForkTest(unittest.TestCase):
+ """Test dealing with Mac OS X style resource forks"""
+ tempdir = rpath.RPath(Globals.local_connection, 'testfiles/output')
+ rf_testdir1 = rpath.RPath(Globals.local_connection,
+ 'testfiles/resource_fork_test1')
+ rf_testdir2 = rpath.RPath(Globals.local_connection,
+ 'testfiles/resource_fork_test2')
+ def make_temp(self):
+ """Make temp directory testfiles/resource_fork_test"""
+ if self.tempdir.lstat(): self.tempdir.delete()
+ self.tempdir.mkdir()
+
+ def testBasic(self):
+ """Test basic reading and writing of resource forks"""
+ self.make_temp()
+ rp = self.tempdir.append('test')
+ rp.touch()
+ assert rp.get_resource_fork() == '', rp.get_resource_fork()
+
+ s = 'new resource fork data'
+ rp.write_resource_fork(s)
+ assert rp.get_resource_fork() == s, rp.get_resource_fork()
+
+ rp2 = self.tempdir.append('test')
+ assert rp2.isreg()
+ assert rp2.get_resource_fork() == s, rp2.get_resource_fork()
+
+ def testRecord(self):
+ """Test reading, writing, and comparing of records with rforks"""
+ self.make_temp()
+ rp = self.tempdir.append('test')
+ rp.touch()
+ rp.set_resource_fork('hello')
+
+ record = metadata.RORP2Record(rp)
+ #print record
+ rorp_out = metadata.Record2RORP(record)
+ assert rorp_out == rp, (rorp_out, rp)
+ assert rorp_out.get_resource_fork() == 'hello'
+
+ def make_backup_dirs(self):
+ """Create testfiles/resource_fork_test[12] dirs for testing"""
+ if self.rf_testdir1.lstat(): self.rf_testdir1.delete()
+ if self.rf_testdir2.lstat(): self.rf_testdir2.delete()
+ self.rf_testdir1.mkdir()
+ rp1_1 = self.rf_testdir1.append('1')
+ rp1_2 = self.rf_testdir1.append('2')
+ rp1_3 = self.rf_testdir1.append('3')
+ rp1_1.touch()
+ rp1_2.touch()
+ rp1_3.symlink('foo')
+ rp1_1.write_resource_fork('This should appear in resource fork')
+ rp1_1.chmod(0400) # test for bug changing resource forks after perms
+ rp1_2.write_resource_fork('Data for the resource fork 2')
+
+
+ self.rf_testdir2.mkdir()
+ rp2_1 = self.rf_testdir2.append('1')
+ rp2_2 = self.rf_testdir2.append('2')
+ rp2_3 = self.rf_testdir2.append('3')
+ rp2_1.touch()
+ rp2_2.touch()
+ rp2_3.touch()
+ rp2_1.write_resource_fork('New data for resource fork 1')
+ rp2_1.chmod(0400)
+ rp2_3.write_resource_fork('New fork')
+
+ def testSeriesLocal(self):
+ """Test backing up and restoring directories with ACLs locally"""
+ Globals.read_resource_forks = Globals.write_resource_forks = 1
+ self.make_backup_dirs()
+ dirlist = ['testfiles/resource_fork_test1', 'testfiles/empty',
+ 'testfiles/resource_fork_test2',
+ 'testfiles/resource_fork_test1']
+ # BackupRestoreSeries(1, 1, dirlist, compare_resource_forks = 1)
+ BackupRestoreSeries(1, 1, dirlist)
+
+ def testSeriesRemote(self):
+ """Test backing up and restoring directories with ACLs locally"""
+ Globals.read_resource_forks = Globals.write_resource_forks = 1
+ self.make_backup_dirs()
+ dirlist = ['testfiles/resource_fork_test1',
+ 'testfiles/resource_fork_test2', 'testfiles/empty',
+ 'testfiles/resource_fork_test1']
+ # BackupRestoreSeries(1, 1, dirlist, compare_resource_forks = 1)
+ BackupRestoreSeries(1, 1, dirlist)
+
+
+if __name__ == "__main__": unittest.main()
+
diff --git a/rdiff-backup/testing/securitytest.py b/rdiff-backup/testing/securitytest.py
index 863d36a..1c7bade 100644
--- a/rdiff-backup/testing/securitytest.py
+++ b/rdiff-backup/testing/securitytest.py
@@ -1,6 +1,6 @@
-import os, unittest
+import os, unittest, time
from commontest import *
-import rdiff_backup.Security
+import rdiff_backup.Security as Security
#Log.setverbosity(5)
@@ -12,7 +12,7 @@ class SecurityTest(unittest.TestCase):
problem.
"""
- assert isinstance(exc, rdiff_backup.Security.Violation)
+ assert isinstance(exc, Security.Violation)
#assert str(exc).find("Security") >= 0, "%s\n%s" % (exc, repr(exc))
def test_vet_request_ro(self):
@@ -56,5 +56,127 @@ class SecurityTest(unittest.TestCase):
SetConnections.CloseConnections()
+ def secure_rdiff_backup(self, in_dir, out_dir, in_local, restrict_args,
+ extra_args = "", success = 1, current_time = None):
+ """Run rdiff-backup locally, with given restrict settings"""
+ if not current_time: current_time = int(time.time())
+ prefix = ('rdiff-backup --current-time %s ' % (current_time,) +
+ '--remote-schema %s ')
+
+ if in_local: out_dir = ("'rdiff-backup %s --server'::%s" %
+ (restrict_args, out_dir))
+ else: in_dir = ("'rdiff-backup %s --server'::%s" %
+ (restrict_args, in_dir))
+
+ cmdline = "%s %s %s %s" % (prefix, extra_args, in_dir, out_dir)
+ print "Executing:", cmdline
+ exit_val = os.system(cmdline)
+ if success: assert not exit_val
+ else: assert exit_val, "Success when wanted failure"
+
+ def test_restrict_positive(self):
+ """Test that --restrict switch doesn't get in the way
+
+ This makes sure that basic backups with the restrict operator
+ work, (initial backup, incremental, restore).
+
+ """
+ Myrm("testfiles/output")
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output', 1,
+ '--restrict testfiles/output',
+ current_time = 10000)
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output', 1,
+ '--restrict testfiles/output')
+
+ Myrm("testfiles/restore_out")
+ self.secure_rdiff_backup('testfiles/output',
+ 'testfiles/restore_out', 1,
+ '--restrict testfiles/restore_out',
+ extra_args = '-r now')
+
+ def test_restrict_negative(self):
+ """Test that --restrict switch denies certain operations"""
+ # Backup to wrong directory
+ Myrm("testfiles/output testfiles/output2")
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output2', 1,
+ '--restrict testfiles/output',
+ success = 0)
+
+ # Restore to wrong directory
+ Myrm("testfiles/output testfiles/restore_out")
+ rdiff_backup(1, 1, 'testfiles/various_file_types',
+ 'testfiles/output')
+ self.secure_rdiff_backup('testfiles/output',
+ 'testfiles/restore_out', 1,
+ '--restrict testfiles/output2',
+ extra_args = '-r now',
+ success = 0)
+
+ # Backup from wrong directory
+ Myrm("testfiles/output")
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output', 0,
+ '--restrict testfiles/foobar',
+ success = 0)
+
+ def test_restrict_readonly_positive(self):
+ """Test that --restrict-read-only switch doesn't impair normal ops"""
+ Myrm("testfiles/output testfiles/restore_out")
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output', 0,
+ '--restrict-read-only testfiles/various_file_types')
+
+ self.secure_rdiff_backup('testfiles/output',
+ 'testfiles/restore_out', 0,
+ '--restrict-read-only testfiles/output',
+ extra_args = '-r now')
+
+ def test_restrict_readonly_negative(self):
+ """Test that --restrict-read-only doesn't allow too much"""
+ # Backup to restricted directory
+ Myrm('testfiles/output')
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output', 1,
+ '--restrict-read-only testfiles/output',
+ success = 0)
+
+ # Restore to restricted directory
+ Myrm('testfiles/output testfiles/restore_out')
+ rdiff_backup(1, 1, 'testfiles/various_file_types', 'testfiles/output')
+ self.secure_rdiff_backup('testfiles/output',
+ 'testfiles/restore_out', 1,
+ '--restrict-read-only testfiles/restore_out',
+ extra_args = '-r now',
+ success = 0)
+
+ def test_restrict_updateonly_positive(self):
+ """Test that --restrict-update-only allows intended use"""
+ Myrm('testfiles/output')
+ rdiff_backup(1, 1, 'testfiles/various_file_types', 'testfiles/output',
+ current_time = 10000)
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output', 1,
+ '--restrict-update-only testfiles/output')
+
+ def test_restrict_updateonly_negative(self):
+ """Test that --restrict-update-only impairs unintended"""
+ Myrm('testfiles/output')
+ self.secure_rdiff_backup('testfiles/various_file_types',
+ 'testfiles/output', 1,
+ '--restrict-update-only testfiles/output',
+ success = 0)
+
+ Myrm('testfiles/output testfiles/restore_out')
+ rdiff_backup(1, 1, 'testfiles/various_file_types', 'testfiles/output')
+ self.secure_rdiff_backup('testfiles/output',
+ 'testfiles/restore_out', 1,
+ '--restrict-update-only testfiles/restore_out',
+ extra_args = '-r now',
+ success = 0)
+
+
if __name__ == "__main__": unittest.main()