summaryrefslogtreecommitdiff
path: root/rdiff-backup/rdiff_backup/fs_abilities.py
diff options
context:
space:
mode:
Diffstat (limited to 'rdiff-backup/rdiff_backup/fs_abilities.py')
-rw-r--r--rdiff-backup/rdiff_backup/fs_abilities.py427
1 files changed, 310 insertions, 117 deletions
diff --git a/rdiff-backup/rdiff_backup/fs_abilities.py b/rdiff-backup/rdiff_backup/fs_abilities.py
index 59a612a..19c2d98 100644
--- a/rdiff-backup/rdiff_backup/fs_abilities.py
+++ b/rdiff-backup/rdiff_backup/fs_abilities.py
@@ -28,11 +28,12 @@ FSAbilities object describing it.
"""
import errno, os
-import Globals, log, TempFile, selection, robust
+import Globals, log, TempFile, selection, robust, SetConnections, \
+ static, FilenameMapping
class FSAbilities:
"""Store capabilities of given file system"""
- chars_to_quote = None # Hold characters not allowable in file names
+ extended_filenames = None # True if filenames can handle ":" etc.
case_sensitive = None # True if "foobar" and "FoObAr" are different files
ownership = None # True if chown works on this filesystem
acls = None # True if access control lists supported
@@ -40,7 +41,7 @@ class FSAbilities:
hardlinks = None # True if hard linking supported
fsync_dirs = None # True if directories can be fsync'd
dir_inc_perms = None # True if regular files can have full permissions
- resource_forks = None # True if regular_file/..namedfork/rsrc holds resource fork
+ resource_forks = None # True if system supports resource forks
carbonfile = None # True if Mac Carbon file data is supported.
name = None # Short string, not used for any technical purpose
read_only = None # True if capabilities were determined non-destructively
@@ -78,21 +79,15 @@ class FSAbilities:
else: return ('Detected abilities for %s file system' %
(read_string,))
- def add_ctq_line():
- """Get line describing chars to quote"""
- ctq_str = (self.chars_to_quote is None and 'N/A'
- or repr(self.chars_to_quote))
- addline('Characters needing quoting', ctq_str)
-
s.append(get_title_line())
if not self.read_only:
- add_ctq_line()
add_boolean_list([('Ownership changing', self.ownership),
('Hard linking', self.hardlinks),
('fsync() directories', self.fsync_dirs),
('Directory inc permissions',
self.dir_inc_perms),
- ('High-bit permissions', self.high_perms)])
+ ('High-bit permissions', self.high_perms),
+ ('Extended filenames', self.extended_filenames)])
add_boolean_list([('Access control lists', self.acls),
('Extended attributes', self.eas),
('Case sensitivity', self.case_sensitive),
@@ -122,20 +117,13 @@ class FSAbilities:
self.set_case_sensitive_readonly(rp)
return self
- def init_readwrite(self, rbdir, use_ctq_file = 1,
- override_chars_to_quote = None):
+ def init_readwrite(self, rbdir):
"""Set variables using fs tested at rp_base. Run locally.
This method creates a temp directory in rp_base and writes to
it in order to test various features. Use on a file system
that will be written to.
- This sets self.chars_to_quote, self.ownership, self.acls,
- self.eas, self.hardlinks, and self.fsync_dirs.
-
- If user_ctq_file is true, try reading the "chars_to_quote"
- file in directory.
-
"""
assert rbdir.conn is Globals.local_connection
if not rbdir.isdir():
@@ -143,9 +131,11 @@ class FSAbilities:
rbdir.mkdir()
self.root_rp = rbdir
self.read_only = 0
-
subdir = TempFile.new_in_dir(rbdir)
subdir.mkdir()
+
+ self.set_extended_filenames(subdir)
+ self.set_case_sensitive_readwrite(subdir)
self.set_ownership(subdir)
self.set_hardlinks(subdir)
self.set_fsync_dirs(subdir)
@@ -155,39 +145,10 @@ class FSAbilities:
self.set_resource_fork_readwrite(subdir)
self.set_carbonfile()
self.set_high_perms_readwrite(subdir)
- if override_chars_to_quote is None: self.set_chars_to_quote(subdir)
- else: self.chars_to_quote = override_chars_to_quote
- if use_ctq_file: self.compare_chars_to_quote(rbdir)
+
subdir.delete()
return self
- def compare_chars_to_quote(self, rbdir):
- """Read chars_to_quote file, compare with current settings"""
- assert self.chars_to_quote is not None
- ctq_rp = rbdir.append("chars_to_quote")
- def write_new_chars():
- """Replace old chars_to_quote file with new value"""
- if ctq_rp.lstat(): ctq_rp.delete()
- fp = ctq_rp.open("wb")
- fp.write(self.chars_to_quote)
- assert not fp.close()
-
- if not ctq_rp.lstat(): write_new_chars()
- else:
- old_chars = ctq_rp.get_data()
- if old_chars != self.chars_to_quote:
- if self.chars_to_quote == "":
- log.Log("Warning: File system no longer needs quoting, "
- "but will retain for backwards compatibility.", 2)
- self.chars_to_quote = old_chars
- else: log.Log.FatalError("""New quoting requirements
-
-This may be caused when you copy an rdiff-backup directory from a
-normal file system on to a windows one that cannot support the same
-characters. If you want to risk it, remove the file
-rdiff-backup-data/chars_to_quote.
-""")
-
def set_ownership(self, testdir):
"""Set self.ownership to true iff testdir's ownership can be changed"""
tmp_rp = testdir.append("foo")
@@ -196,7 +157,7 @@ rdiff-backup-data/chars_to_quote.
try:
tmp_rp.chown(uid+1, gid+1) # just choose random uid/gid
tmp_rp.chown(0, 0)
- except (IOError, OSError), exc: self.ownership = 0
+ except (IOError, OSError): self.ownership = 0
else: self.ownership = 1
tmp_rp.delete()
@@ -209,7 +170,7 @@ rdiff-backup-data/chars_to_quote.
hl_dest.hardlink(hl_source.path)
if hl_source.getinode() != hl_dest.getinode():
raise IOError(errno.EOPNOTSUPP, "Hard links don't compare")
- except (IOError, OSError), exc:
+ except (IOError, OSError):
log.Log("Warning: hard linking not supported by filesystem "
"at %s" % (self.root_rp.path,), 3)
self.hardlinks = 0
@@ -219,58 +180,34 @@ rdiff-backup-data/chars_to_quote.
"""Set self.fsync_dirs if directories can be fsync'd"""
assert testdir.conn is Globals.local_connection
try: testdir.fsync()
- except (IOError, OSError), exc:
+ except (IOError, OSError):
log.Log("Directories on file system at %s are not fsyncable.\n"
"Assuming it's unnecessary." % (testdir.path,), 4)
self.fsync_dirs = 0
else: self.fsync_dirs = 1
- def set_chars_to_quote(self, subdir):
- """Set self.chars_to_quote by trying to write various paths"""
- def is_case_sensitive():
- """Return true if file system is case sensitive"""
- upper_a = subdir.append("A")
- upper_a.touch()
- lower_a = subdir.append("a")
- if lower_a.lstat():
- lower_a.delete()
- upper_a.setdata()
- assert not upper_a.lstat()
- self.case_sensitive = 0
- else:
- upper_a.delete()
- self.case_sensitive = 1
- return self.case_sensitive
-
- def supports_unusual_chars():
- """Test handling of several chars sometimes not supported"""
- for filename in [':', '\\', chr(175)]:
- try:
- rp = subdir.append(filename)
- rp.touch()
- except (IOError, OSError):
- assert not rp.lstat()
- return 0
- else:
- assert rp.lstat()
- rp.delete()
- return 1
+ def set_extended_filenames(self, subdir):
+ """Set self.extended_filenames by trying to write a path"""
+ assert not self.read_only
+
+ # Make sure ordinary filenames ok
+ ordinary_filename = '5-_ a.'
+ ord_rp = subdir.append(ordinary_filename)
+ ord_rp.touch()
+ assert ord_rp.lstat()
+ ord_rp.delete()
- def sanity_check():
- """Make sure basic filenames writable"""
- for filename in ['5-_ a.']:
- rp = subdir.append(filename)
- rp.touch()
- assert rp.lstat()
- rp.delete()
-
- sanity_check()
- if is_case_sensitive():
- if supports_unusual_chars(): self.chars_to_quote = ""
- else: self.chars_to_quote = "^A-Za-z0-9_ -."
+ extended_filename = ':\\' + chr(175)
+ try:
+ ext_rp = subdir.append(extended_filename)
+ ext_rp.touch()
+ except (IOError, OSError):
+ assert not ext_rp.lstat()
+ self.extended_filenames = 0
else:
- if supports_unusual_chars(): self.chars_to_quote = "A-Z;"
- else: self.chars_to_quote = "^a-z0-9_ -."
+ assert ext_rp.lstat()
+ ext_rp.delete()
+ self.extended_filenames = 1
def set_acls(self, rp):
"""Set self.acls based on rp. Does not write. Needs to be local"""
@@ -285,11 +222,26 @@ rdiff-backup-data/chars_to_quote.
return
try: posix1e.ACL(file=rp.path)
- except IOError, exc:
+ except IOError:
log.Log("ACLs not supported by filesystem at %s" % (rp.path,), 4)
self.acls = 0
else: self.acls = 1
+ def set_case_sensitive_readwrite(self, subdir):
+ """Determine if directory at rp is case sensitive by writing"""
+ assert not self.read_only
+ upper_a = subdir.append("A")
+ upper_a.touch()
+ lower_a = subdir.append("a")
+ if lower_a.lstat():
+ lower_a.delete()
+ upper_a.setdata()
+ assert not upper_a.lstat()
+ self.case_sensitive = 0
+ else:
+ upper_a.delete()
+ self.case_sensitive = 1
+
def set_case_sensitive_readonly(self, rp):
"""Determine if directory at rp is case sensitive without writing"""
def find_letter(subdir):
@@ -349,7 +301,7 @@ rdiff-backup-data/chars_to_quote.
if write:
xattr.setxattr(rp.path, "user.test", "test val")
assert xattr.getxattr(rp.path, "user.test") == "test val"
- except IOError, exc:
+ except IOError:
log.Log("Extended attributes not supported by "
"filesystem at %s" % (rp.path,), 4)
self.eas = 0
@@ -402,7 +354,7 @@ rdiff-backup-data/chars_to_quote.
fp_read = open(os.path.join(reg_rp.path, '..namedfork', 'rsrc'), 'rb')
s_back = fp_read.read()
assert not fp_read.close()
- except (OSError, IOError), e: self.resource_forks = 0
+ except (OSError, IOError): self.resource_forks = 0
else: self.resource_forks = (s_back == s)
reg_rp.delete()
@@ -421,7 +373,7 @@ rdiff-backup-data/chars_to_quote.
fp = rfork.open('rb')
fp.read()
assert not fp.close()
- except (OSError, IOError), e:
+ except (OSError, IOError):
self.resource_forks = 0
return
self.resource_forks = 1
@@ -435,27 +387,268 @@ rdiff-backup-data/chars_to_quote.
try:
tmp_rp.chmod(07000)
tmp_rp.chmod(07777)
- except (OSError, IOError), e: self.high_perms = 0
+ except (OSError, IOError): self.high_perms = 0
else: self.high_perms = 1
tmp_rp.delete()
-def get_fsabilities_readonly(desc_string, rp):
- """Return an FSAbilities object with given description_string
+def get_readonly_fsa(desc_string, rp):
+ """Return an fsa with given description_string
- Will be initialized read_only with given RPath rp.
+ Will be initialized read_only with given RPath rp. We separate
+ this out into a separate function so the request can be vetted by
+ the security module.
"""
return FSAbilities(desc_string).init_readonly(rp)
-def get_fsabilities_readwrite(desc_string, rb, use_ctq_file = 1, ctq = None):
- """Like above but initialize read/write and pass other arguments"""
- return FSAbilities(desc_string).init_readwrite(
- rb, use_ctq_file = use_ctq_file, override_chars_to_quote = ctq)
-
-def get_fsabilities_restoresource(rp):
- """Used when restoring, get abilities of source directory"""
- fsa = FSAbilities('source').init_readonly(rp)
- ctq_rp = rp.append("chars_to_quote")
- if ctq_rp.lstat(): fsa.chars_to_quote = ctq_rp.get_data()
- else: fsa.chars_to_quote = ""
- return fsa
+
+class SetGlobals:
+ """Various functions for setting Globals vars given FSAbilities above
+
+ Container for BackupSetGlobals and RestoreSetGlobals (don't use directly)
+
+ """
+ def __init__(self, in_conn, out_conn, src_fsa, dest_fsa):
+ """Just store some variables for use below"""
+ self.in_conn, self.out_conn = in_conn, out_conn
+ self.src_fsa, self.dest_fsa = src_fsa, dest_fsa
+
+ def set_eas(self):
+ self.update_triple(self.src_fsa.eas, self.dest_fsa.eas,
+ ('eas_active', 'eas_write', 'eas_conn'))
+
+ def set_acls(self):
+ self.update_triple(self.src_fsa.acls, self.dest_fsa.acls,
+ ('acls_active', 'acls_write', 'acls_conn'))
+ if Globals.never_drop_acls and not Globals.acls_active:
+ Log.FatalError("--never-drop-acls specified, but ACL support\n"
+ "missing from destination filesystem")
+
+ def set_resource_forks(self):
+ self.update_triple(self.src_fsa.resource_forks,
+ self.dest_fsa.resource_forks,
+ ('resource_forks_active', 'resource_forks_write',
+ 'resource_forks_conn'))
+
+ def set_carbonfile(self):
+ self.update_triple(self.src_fsa.carbonfile, self.dest_fsa.carbonfile,
+ ('carbonfile_active', 'carbonfile_write', 'carbonfile_conn'))
+ if self.src_fsa.carbonfile and not Globals.carbonfile_active:
+ Log("Source may have carbonfile support, but support defaults to "
+ "off.\n Use --carbonfile to enable.", 5)
+
+ def set_hardlinks(self):
+ if Globals.preserve_hardlinks != 0:
+ SetConnections.UpdateGlobal('preserve_hardlinks',
+ self.dest_fsa.hardlinks)
+
+ def set_fsync_directories(self):
+ SetConnections.UpdateGlobal('fsync_directories',
+ self.dest_fsa.fsync_dirs)
+
+ def set_change_ownership(self):
+ SetConnections.UpdateGlobal('change_ownership',
+ self.dest_fsa.ownership)
+
+ def set_high_perms(self):
+ if not self.dest_fsa.high_perms:
+ SetConnections.UpdateGlobal('permission_mask', 0777)
+
+
+class BackupSetGlobals(SetGlobals):
+ """Functions for setting fsa related globals for backup session"""
+ def update_triple(self, src_support, dest_support, attr_triple):
+ """Many of the settings have a common form we can handle here"""
+ active_attr, write_attr, conn_attr = attr_triple
+ if Globals.get(active_attr) == 0: return # don't override 0
+ for attr in attr_triple: SetConnections.UpdateGlobal(attr, None)
+ if not src_support: return # if source doesn't support, nothing
+ SetConnections.UpdateGlobal(active_attr, 1)
+ self.in_conn.Globals.set_local(conn_attr, 1)
+ if dest_support:
+ SetConnections.UpdateGlobal(write_attr, 1)
+ self.out_conn.Globals.set_local(conn_attr, 1)
+
+ def set_chars_to_quote(self, rbdir):
+ """Set chars_to_quote setting for backup session
+
+ Unlike the other options, the chars_to_quote setting also
+ depends on the current settings in the rdiff-backup-data
+ directory, not just the current fs features.
+
+ """
+ ctq = self.compare_ctq_file(rbdir, self.get_ctq_from_fsas())
+
+ SetConnections.UpdateGlobal('chars_to_quote', ctq)
+ if Globals.chars_to_quote: FilenameMapping.set_init_quote_vals()
+
+ def get_ctq_from_fsas(self):
+ """Determine chars_to_quote just from filesystems, no ctq file"""
+ if not self.src_fsa.case_sensitive and self.dest_fsa.case_sensitive:
+ if self.dest_fsa.extended_filenames:
+ return "A-Z;" # Quote upper case and quoting char
+ else: return "^a-z0-9_ -." # quote everything but basic chars
+
+ if self.dest_fsa.extended_filenames:
+ return "" # Don't quote anything
+ else: return "^A-Za-z0-9_ -."
+
+ def compare_ctq_file(self, rbdir, suggested_ctq):
+ """Compare ctq file with suggested result, return actual ctq"""
+ ctq_rp = rbdir.append("chars_to_quote")
+ if not ctq_rp.lstat():
+ if Globals.chars_to_quote is None: actual_ctq = suggested_ctq
+ else: actual_ctq = Globals.chars_to_quote
+ ctq_rp.write_string(actual_ctq)
+ return actual_ctq
+
+ if Globals.chars_to_quote is None: actual_ctq = ctq_rp.get_data()
+ else: actual_ctq = Globals.chars_to_quote # Globals override
+
+ if actual_ctq == suggested_ctq: return actual_ctq
+ if suggested_ctq == "":
+ log.Log("Warning: File system no longer needs quoting, "
+ "but we will retain for backwards compatibility.", 2)
+ return actual_ctq
+ log.Log.FatalError("""New quoting requirements!
+
+The quoting this session appears to need do not match those in
+
+%s
+
+This may be caused when you copy an rdiff-backup repository from a
+normal file system onto a windows one that cannot support the same
+characters, or if you backup a case-sensitive file system onto a
+case-insensitive one that previously only had case-insensitive ones
+backed up onto it.
+
+If you want to risk it, remove the file
+rdiff-backup-data/chars_to_quote.""" % (ctq_rp.path,))
+
+
+class RestoreSetGlobals(SetGlobals):
+ """Functions for setting fsa-related globals for restore session"""
+ def update_triple(self, src_support, dest_support, attr_triple):
+ """Update global settings for feature based on fsa results
+
+ This is slightly different from BackupSetGlobals.update_triple
+ because (using the mirror_metadata file) rpaths from the
+ source may have more information than the file system
+ supports.
+
+ """
+ active_attr, write_attr, conn_attr = attr_triple
+ if Globals.get(active_attr) == 0: return # don't override 0
+ for attr in attr_triple: SetConnections.UpdateGlobal(attr, None)
+ if not dest_support: return # if dest doesn't support, do nothing
+ SetConnections.UpdateGlobal(active_attr, 1)
+ self.out_conn.Globals.set_local(conn_attr, 1)
+ self.out_conn.Globals.set_local(write_attr, 1)
+ if src_support: self.in_conn.Globals.set_local(conn_attr, 1)
+
+ def set_chars_to_quote(self, rbdir):
+ """Set chars_to_quote from rdiff-backup-data dir"""
+ if Globals.chars_to_quote is not None: return # already overridden
+
+ ctq_rp = rbdir.append("chars_to_quote")
+ if ctq_rp.lstat():
+ SetConnections.UpdateGlobal("chars_to_quote", ctq_rp.get_data())
+ else:
+ log.Log("Warning: chars_to_quote file not found,\n"
+ "assuming no quoting in backup repository.", 2)
+ SetConnections.UpdateGlobal("chars_to_quote", "")
+
+
+class SingleSetGlobals(RestoreSetGlobals):
+ """For setting globals when dealing only with one filesystem"""
+ def __init__(self, conn, fsa):
+ self.conn = conn
+ self.dest_fsa = fsa
+
+ def update_triple(self, fsa_support, attr_triple):
+ """Update global vars from single fsa test"""
+ active_attr, write_attr, conn_attr = attr_triple
+ if Globals.get(active_attr) == 0: return # don't override 0
+ for attr in attr_triple: SetConnections.UpdateGlobal(attr, None)
+ if not fsa_support: return
+ SetConnections.UpdateGlobal(active_attr, 1)
+ SetConnections.UpdateGlobal(write_attr, 1)
+ self.conn.Globals.set_local(conn_attr, 1)
+
+ def set_eas(self):
+ self.update_triple(self.dest_fsa.eas,
+ ('eas_active', 'eas_write', 'eas_conn'))
+ def set_acls(self):
+ self.update_triple(self.dest_fsa.acls,
+ ('acls_active', 'acls_write', 'acls_conn'))
+ def set_resource_forks(self):
+ self.update_triple(self.dest_fsa.resource_forks,
+ ('resource_forks_active',
+ 'resource_forks_write', 'resource_forks_conn'))
+ def set_carbonfile(self):
+ self.update_triple(self.dest_fsa.carbonfile,
+ ('carbonfile_active', 'carbonfile_write', 'carbonfile_conn'))
+
+
+def backup_set_globals(rpin):
+ """Given rps for source filesystem and repository, set fsa globals
+
+ This should be run on the destination connection, because we may
+ need to write a new chars_to_quote file.
+
+ """
+ assert Globals.rbdir.conn is Globals.local_connection
+ src_fsa = rpin.conn.fs_abilities.get_readonly_fsa('source', rpin)
+ log.Log(str(src_fsa), 4)
+ dest_fsa = FSAbilities('destination').init_readwrite(Globals.rbdir)
+ log.Log(str(dest_fsa), 4)
+
+ bsg = BackupSetGlobals(rpin.conn, Globals.rbdir.conn, src_fsa, dest_fsa)
+ bsg.set_eas()
+ bsg.set_acls()
+ bsg.set_resource_forks()
+ bsg.set_carbonfile()
+ bsg.set_hardlinks()
+ bsg.set_fsync_directories()
+ bsg.set_change_ownership()
+ bsg.set_high_perms()
+ bsg.set_chars_to_quote(Globals.rbdir)
+
+def restore_set_globals(rpout):
+ """Set fsa related globals for restore session, given in/out rps"""
+ assert rpout.conn is Globals.local_connection
+ src_fsa = Globals.rbdir.conn.fs_abilities.get_readonly_fsa(
+ 'rdiff-backup repository', Globals.rbdir)
+ log.Log(str(src_fsa), 4)
+ dest_fsa = FSAbilities('restore target').init_readwrite(rpout)
+ log.Log(str(dest_fsa), 4)
+
+ rsg = RestoreSetGlobals(Globals.rbdir.conn, rpout.conn, src_fsa, dest_fsa)
+ rsg.set_eas()
+ rsg.set_acls()
+ rsg.set_resource_forks()
+ rsg.set_carbonfile()
+ rsg.set_hardlinks()
+ # No need to fsync anything when restoring
+ rsg.set_change_ownership()
+ rsg.set_high_perms()
+ rsg.set_chars_to_quote(Globals.rbdir)
+
+def single_set_globals(rp, read_only = None):
+ """Set fsa related globals for operation on single filesystem"""
+ if read_only:
+ fsa = rp.conn.fs_abilities.get_readonly_fsa(rp.path, rp)
+ else: fsa = FSAbilities(rp.path).init_readwrite(rp)
+ log.Log(str(fsa), 4)
+
+ ssg = SingleSetGlobals(rp.conn, fsa)
+ ssg.set_eas()
+ ssg.set_acls()
+ ssg.set_resource_forks()
+ ssg.set_carbonfile()
+ if not read_only:
+ ssg.set_hardlinks()
+ ssg.set_change_ownership()
+ ssg.set_high_perms()
+ ssg.set_chars_to_quote(Globals.rbdir)
+