From 5e09353fe5ce80b724a30db11cb10523d147c6ec Mon Sep 17 00:00:00 2001 From: bescoto Date: Mon, 30 Jun 2003 03:00:18 +0000 Subject: Many changes - added extended attribute support and file system ability detection git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup/trunk@334 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109 --- rdiff-backup/TODO | 2 + rdiff-backup/rdiff-backup.1 | 43 +--- rdiff-backup/rdiff_backup/FilenameMapping.py | 20 +- rdiff-backup/rdiff_backup/Globals.py | 18 +- rdiff-backup/rdiff_backup/Main.py | 327 ++++++++++++++------------- rdiff-backup/rdiff_backup/Security.py | 5 +- rdiff-backup/rdiff_backup/SetConnections.py | 7 +- rdiff-backup/rdiff_backup/backup.py | 39 +++- rdiff-backup/rdiff_backup/connection.py | 7 +- rdiff-backup/rdiff_backup/eas_acls.py | 185 +++++++++++++++ rdiff-backup/rdiff_backup/fs_abilities.py | 146 ++++++------ rdiff-backup/rdiff_backup/metadata.py | 222 ++++++++++-------- rdiff-backup/rdiff_backup/regress.py | 3 +- rdiff-backup/rdiff_backup/restore.py | 13 +- rdiff-backup/rdiff_backup/rpath.py | 39 +++- rdiff-backup/testing/commontest.py | 41 ++-- rdiff-backup/testing/eas_aclstest.py | 135 +++++++++++ rdiff-backup/testing/finaltest.py | 27 ++- rdiff-backup/testing/metadatatest.py | 40 +++- rdiff-backup/testing/regressiontest.py | 14 +- rdiff-backup/testing/restoretest.py | 16 -- 21 files changed, 905 insertions(+), 444 deletions(-) create mode 100644 rdiff-backup/rdiff_backup/eas_acls.py create mode 100644 rdiff-backup/testing/eas_aclstest.py diff --git a/rdiff-backup/TODO b/rdiff-backup/TODO index 54d091b..33191ee 100644 --- a/rdiff-backup/TODO +++ b/rdiff-backup/TODO @@ -1,4 +1,6 @@ +Use ctime to check whether files have been changed +Include some option to summarize space taken up ---------[ Medium term ]--------------------------------------- diff --git a/rdiff-backup/rdiff-backup.1 b/rdiff-backup/rdiff-backup.1 index b6ecbe4..08469bf 100644 --- a/rdiff-backup/rdiff-backup.1 +++ b/rdiff-backup/rdiff-backup.1 @@ -56,23 +56,14 @@ ability to restore previous versions of that file. .SH OPTIONS .TP .B -b, --backup-mode -Force backup mode even if first argument appears to be an increment file. +Force backup mode even if first argument appears to be an increment or +mirror file. .TP .B --calculate-average Enter calculate average mode. The arguments should be a number of statistics files. rdiff-backup will print the average of the listed statistics files and exit. .TP -.BI "--chars-to-quote " chars -If this option is set, any characters in -.I chars -present in filenames on the source side will be quoted on the -destination side, so that they do not appear in filenames on the -remote side. See -.B --quoting-char -and -.BR --windows-mode . -.TP .B --check-destination-dir If an rdiff-backup session fails, running rdiff-backup with this option on the destination dir will undo the failed directory. This @@ -132,8 +123,7 @@ See the section for more information. .TP .B --exclude-special-files -Exclude all device files, fifos, sockets, and symlinks. This option -is implied by --windows-mode. +Exclude all device files, fifos, sockets, and symlinks. .TP .B --force Authorize the updating or overwriting of a destination path. @@ -202,7 +192,7 @@ In this mode rdiff-backup is similar to rsync (but usually slower). .TP .B --no-compare-inode -This relative esoteric option prevents rdiff-backup from flagging a +This relatively esoteric option prevents rdiff-backup from flagging a file as changed when its inode changes. This option may be useful if you are backing up two different directories to the same rdiff-backup destination directory. The downside is that hard link information may @@ -255,13 +245,6 @@ session statistics file. See the .B STATISTICS section for more information. .TP -.BI "--quoting-char " char -Use the specified character for quoting characters specified to be -escaped by the -.B --chars-to-quote -option. The default is the semicolon ";". See also -.BR --windows-mode . -.TP .BI "-r, --restore-as-of " restore_time Restore the specified directory as it was as of .IR restore_time . @@ -273,10 +256,6 @@ and see the .B RESTORING section for more information on restoring. .TP -.BI "--remote-cmd " command -This command has been depreciated as of version 0.4.1. Use ---remote-schema instead. -.TP .BI "--remote-schema " schema Specify an alternate method of connecting to a remote computer. This is necessary to get rdiff-backup not to use ssh for remote backups, or @@ -354,20 +333,6 @@ is noisiest). This determines how much is written to the log file. .TP .B "-V, --version" Print the current version and exit -.TP -.B --windows-mode -This option quotes characters not allowable on windows, and does not -try to preserve ownership, hardlinks, or permissions on the -destination side. It is appropriate when backing up a normal unix -file system to a windows one such as VFS, or a file system with -similar limitations. Because metadata is stored in a separate regular -file, this option does not prevent all data from being restored. -.TP -.B --windows-restore -This option turns on windows quoting, but does not disable -permissions, hard linking, or ownership. Use this when restoring from -an rdiff-backup directory on a windows file system to a unix file -system. .SH EXAMPLES Simplest case---backup directory foo to directory bar, with increments diff --git a/rdiff-backup/rdiff_backup/FilenameMapping.py b/rdiff-backup/rdiff_backup/FilenameMapping.py index 117d711..066b86a 100644 --- a/rdiff-backup/rdiff_backup/FilenameMapping.py +++ b/rdiff-backup/rdiff_backup/FilenameMapping.py @@ -1,4 +1,4 @@ -# Copyright 2002 Ben Escoto +# Copyright 2002, 2003 Ben Escoto # # This file is part of rdiff-backup. # @@ -29,7 +29,7 @@ handle that error.) """ -import re +import re, types import Globals, log, rpath max_filename_length = 255 @@ -66,12 +66,14 @@ def set_init_quote_vals_local(): def init_quoting_regexps(): """Compile quoting regular expressions""" global chars_to_quote_regexp, unquoting_regexp + assert chars_to_quote and type(chars_to_quote) is types.StringType, \ + "Chars to quote: '%s'" % (chars_to_quote,) try: chars_to_quote_regexp = \ re.compile("[%s]|%s" % (chars_to_quote, quoting_char), re.S) unquoting_regexp = re.compile("%s[0-9]{3}" % quoting_char, re.S) except re.error: - log.Log.FatalError("Error '%s' when processing char quote list %s" % + log.Log.FatalError("Error '%s' when processing char quote list '%s'" % (re.error, chars_to_quote)) def quote(path): @@ -131,8 +133,16 @@ class QuotedRPath(rpath.RPath): def isincfile(self): """Return true if path indicates increment, sets various variables""" - result = rpath.RPath.isincfile(self) - if result: self.inc_basestr = unquote(self.inc_basestr) + if not self.index: # consider the last component as quoted + dirname, basename = self.dirsplit() + temp_rp = rpath.RPath(self.conn, dirname, (unquote(basename),)) + result = temp_rp.isincfile() + if result: + self.inc_basestr = unquote(temp_rp.inc_basestr) + self.inc_timestr = unquote(temp_rp.inc_timestr) + else: + result = rpath.RPath.isincfile(self) + if result: self.inc_basestr = unquote(self.inc_basestr) return result def get_quotedrpath(rp, separate_basename = 0): diff --git a/rdiff-backup/rdiff_backup/Globals.py b/rdiff-backup/rdiff_backup/Globals.py index 0d83d4c..2f60ab7 100644 --- a/rdiff-backup/rdiff_backup/Globals.py +++ b/rdiff-backup/rdiff_backup/Globals.py @@ -66,6 +66,14 @@ change_mirror_perms = (process_uid != 0) # If true, try to reset the atimes of the source partition. preserve_atime = None +# If true, save the extended attributes when backing up. +read_eas = None + +# If true, preserve the extended attributes on the mirror directory +# when backing up, or write them to the restore directory. This +# implies read_eas. +write_eas = None + # This will be set as soon as the LocalConnection class loads local_connection = None @@ -112,10 +120,12 @@ rbdir = None # quoting_enabled is true if we should quote certain characters in # filenames on the source side (see FilenameMapping for more -# info). chars_to_quote is a string whose characters should be -# quoted, and quoting_char is the character to quote with. -quoting_enabled = None -chars_to_quote = "A-Z:" +# info). + +# chars_to_quote is a string whose characters should be quoted. It +# should be true if certain characters in filenames on the source side +# should be escaped (see FilenameMapping for more info). +chars_to_quote = None quoting_char = ';' # If true, emit output intended to be easily readable by a diff --git a/rdiff-backup/rdiff_backup/Main.py b/rdiff-backup/rdiff_backup/Main.py index 5a03fb5..505025d 100644 --- a/rdiff-backup/rdiff_backup/Main.py +++ b/rdiff-backup/rdiff_backup/Main.py @@ -1,4 +1,4 @@ -# Copyright 2002 Ben Escoto +# Copyright 2002, 2003 Ben Escoto # # This file is part of rdiff-backup. # @@ -24,7 +24,7 @@ import getopt, sys, re, os from log import Log, LoggerError, ErrorLog import Globals, Time, SetConnections, selection, robust, rpath, \ manage, backup, connection, restore, FilenameMapping, \ - Security, Hardlink, regress, C + Security, Hardlink, regress, C, fs_abilities action = None @@ -32,6 +32,9 @@ remote_cmd, remote_schema = None, None force = None select_opts = [] select_files = [] +# These are global because they are set while we are trying to figure +# whether to restore or to backup +restore_root, restore_index, restore_root_set = None, None, 0 def parse_cmdlineoptions(arglist): """Parse argument list and set global preferences""" @@ -43,24 +46,24 @@ def parse_cmdlineoptions(arglist): except IOError: Log.FatalError("Error opening file %s" % filename) try: optlist, args = getopt.getopt(arglist, "blr:sv:V", - ["backup-mode", "calculate-average", "chars-to-quote=", - "check-destination-dir", "current-time=", "exclude=", - "exclude-device-files", "exclude-filelist=", - "exclude-filelist-stdin", "exclude-globbing-filelist=", - "exclude-mirror=", "exclude-other-filesystems", - "exclude-regexp=", "exclude-special-files", "force", - "include=", "include-filelist=", "include-filelist-stdin", + ["backup-mode", "calculate-average", "check-destination-dir", + "current-time=", "exclude=", "exclude-device-files", + "exclude-filelist=", "exclude-filelist-stdin", + "exclude-globbing-filelist=", "exclude-mirror=", + "exclude-other-filesystems", "exclude-regexp=", + "exclude-special-files", "force", "include=", + "include-filelist=", "include-filelist-stdin", "include-globbing-filelist=", "include-regexp=", "list-at-time=", "list-changed-since=", "list-increments", "no-compare-inode", "no-compression", "no-compression-regexp=", "no-file-statistics", - "no-hard-links", "null-separator", "parsable-output", - "print-statistics", "quoting-char=", "remote-cmd=", - "remote-schema=", "remove-older-than=", "restore-as-of=", - "restrict=", "restrict-read-only=", "restrict-update-only=", - "server", "ssh-no-compression", "terminal-verbosity=", - "test-server", "verbosity=", "version", "windows-mode", - "windows-restore"]) + "no-hard-links", "null-separator", + "override-chars-to-quote=", "parsable-output", + "print-statistics", "remote-cmd=", "remote-schema=", + "remove-older-than=", "restore-as-of=", "restrict=", + "restrict-read-only=", "restrict-update-only=", "server", + "ssh-no-compression", "terminal-verbosity=", "test-server", + "verbosity=", "version"]) except getopt.error, e: commandline_error("Bad commandline options: %s" % str(e)) @@ -68,9 +71,6 @@ def parse_cmdlineoptions(arglist): if opt == "-b" or opt == "--backup-mode": action = "backup" elif opt == "--calculate-average": action = "calculate-average" elif opt == "--check-destination-dir": action = "check-destination-dir" - elif opt == "--chars-to-quote": - Globals.set('chars_to_quote', arg) - Globals.set('quoting_enabled', 1) elif opt == "--current-time": Globals.set_integer('current_time', arg) elif opt == "--exclude": select_opts.append((opt, arg)) @@ -112,11 +112,10 @@ def parse_cmdlineoptions(arglist): elif opt == "--no-file-statistics": Globals.set('file_statistics', 0) elif opt == "--no-hard-links": Globals.set('preserve_hardlinks', 0) elif opt == "--null-separator": Globals.set("null_separator", 1) + elif opt == "--override-chars-to-quote": + Globals.set('chars_to_quote', arg) elif opt == "--parsable-output": Globals.set('parsable_output', 1) elif opt == "--print-statistics": Globals.set('print_statistics', 1) - elif opt == "--quoting-char": - Globals.set('quoting_char', arg) - Globals.set('quoting_enabled', 1) elif opt == "-r" or opt == "--restore-as-of": restore_timestr, action = arg, "restore-as-of" elif opt == "--remote-cmd": remote_cmd = arg @@ -142,55 +141,30 @@ def parse_cmdlineoptions(arglist): print "rdiff-backup " + Globals.version sys.exit(0) elif opt == "-v" or opt == "--verbosity": Log.setverbosity(arg) - elif opt == "--windows-mode": - Globals.set('chars_to_quote', "^a-z0-9._ -") - Globals.set('quoting_enabled', 1) - Globals.set('preserve_hardlinks', 0) - Globals.set('change_ownership', 0) - Globals.set('change_permissions', 0) - Globals.set('fsync_directories', 0) - elif opt == '--windows-restore': - Globals.set('chars_to_quote', "^a-z0-9._ -") - Globals.set('quoting_enabled', 1) else: Log.FatalError("Unknown option %s" % opt) -def isincfilename(path): - """Return true if path is of a (possibly quoted) increment file""" - rp = rpath.RPath(Globals.local_connection, path) - if Globals.quoting_enabled: - if not FilenameMapping.quoting_char: - FilenameMapping.set_init_quote_vals() - rp = FilenameMapping.get_quotedrpath(rp, separate_basename = 1) - result = rp.isincfile() - return result - -def set_action(): - """Check arguments and try to set action""" +def check_action(): + """Check to make sure action is compatible with args""" global action + arg_action_dict = {0: ['server'], + 1: ['list-increments', 'remove-older-than', + 'list-at-time', 'list-changed-since', + 'check-destination-dir'], + 2: ['backup', 'restore', 'restore-as-of']} l = len(args) - if not action: + if not action: assert l == 2, args # cannot tell backup or restore yet + elif action == 'calculate-average': if l == 0: commandline_error("No arguments given") - elif l == 1: action = "restore" - elif l == 2: - if isincfilename(args[0]): action = "restore" - else: action = "backup" - else: commandline_error("Too many arguments given") - - if l == 0 and action != "server": - commandline_error("No arguments given") - if l > 0 and action == "server": - commandline_error("Too many arguments given") - if l < 2 and (action == "backup" or action == "restore-as-of"): - commandline_error("Two arguments are required (source, destination).") - if l == 2 and (action == "list-increments" or - action == "remove-older-than" or - action == "list-at-time" or - action == "list-changed-since" or - action == "check-destination-dir"): - commandline_error("Only use one argument, " - "the root of the backup directory") - if l > 2 and action != "calculate-average": - commandline_error("Too many arguments given") + elif l > 2 or action not in arg_action_dict[l]: + commandline_error("Wrong number of arguments given. See man page.") + +def final_set_action(rps): + """If no action set, decide between backup and restore at this point""" + global action + if action: return + assert len(rps) == 2, rps + if restore_get_root(rps[0]): action = "restore" + else: action = "backup" def commandline_error(message): sys.stderr.write("Error: %s\n" % message) @@ -201,7 +175,6 @@ def misc_setup(rps): """Set default change ownership flag, umask, relay regexps""" os.umask(077) Time.setcurtime(Globals.current_time) - FilenameMapping.set_init_quote_vals() SetConnections.UpdateGlobal("client_conn", Globals.local_connection) Globals.postset_regexp('no_compression_regexp', Globals.no_compression_regexp_string) @@ -216,7 +189,7 @@ def take_action(rps): sys.exit(0) elif action == "backup": Backup(rps[0], rps[1]) elif action == "restore": Restore(*rps) - elif action == "restore-as-of": RestoreAsOf(rps[0], rps[1]) + elif action == "restore-as-of": Restore(rps[0], rps[1], 1) elif action == "test-server": SetConnections.TestConnections() elif action == "list-at-time": ListAtTime(rps[0]) elif action == "list-changed-since": ListChangedSince(rps[0]) @@ -236,10 +209,11 @@ def cleanup(): def Main(arglist): """Start everything up!""" parse_cmdlineoptions(arglist) - set_action() + check_action() cmdpairs = SetConnections.get_cmd_pairs(args, remote_schema, remote_cmd) - Security.initialize(action, cmdpairs) + Security.initialize(action or "mirror", cmdpairs) rps = map(SetConnections.cmdpair2rp, cmdpairs) + final_set_action(rps) misc_setup(rps) take_action(rps) cleanup() @@ -247,11 +221,15 @@ def Main(arglist): def Backup(rpin, rpout): """Backup, possibly incrementally, src_path to dest_path.""" - if Globals.quoting_enabled: - rpout = FilenameMapping.get_quotedrpath(rpout) SetConnections.BackupInitConnections(rpin.conn, rpout.conn) + backup_check_dirs(rpin, rpout) + backup_set_fs_globals(rpin, rpout) + if Globals.chars_to_quote: + rpout = FilenameMapping.get_quotedrpath(rpout) + SetConnections.UpdateGlobal( + 'rbdir', FilenameMapping.get_quotedrpath(Globals.rbdir)) + backup_set_rbdir(rpin, rpout) backup_set_select(rpin) - backup_init_dirs(rpin, rpout) if prevtime: rpout.conn.Main.backup_touch_curmirror_local(rpin, rpout) Time.setprevtime(prevtime) @@ -266,32 +244,39 @@ def backup_set_select(rpin): rpin.conn.backup.SourceStruct.set_source_select(rpin, select_opts, *select_files) -def backup_init_dirs(rpin, rpout): - """Make sure rpin and rpout are valid, init data dir and logging""" - global datadir, incdir, prevtime +def backup_check_dirs(rpin, rpout): + """Make sure in and out dirs exist and are directories""" if rpout.lstat() and not rpout.isdir(): if not force: Log.FatalError("Destination %s exists and is not a " "directory" % rpout.path) else: Log("Deleting %s" % rpout.path, 3) rpout.delete() + if not rpout.lstat(): + try: rpout.mkdir() + except os.error: + Log.FatalError("Unable to create directory %s" % rpout.path) if not rpin.lstat(): Log.FatalError("Source directory %s does not exist" % rpin.path) elif not rpin.isdir(): Log.FatalError("Source %s is not a directory" % rpin.path) + backup_warn_if_infinite_regress(rpin, rpout) + Globals.rbdir = rpout.append_path("rdiff-backup-data") - datadir = rpout.append_path("rdiff-backup-data") - SetConnections.UpdateGlobal('rbdir', datadir) +def backup_set_rbdir(rpin, rpout): + """Initialize data dir and logging""" + global incdir, prevtime + SetConnections.UpdateGlobal('rbdir', Globals.rbdir) checkdest_if_necessary(rpout) - incdir = datadir.append_path("increments") + incdir = Globals.rbdir.append_path("increments") prevtime = backup_get_mirrortime() - if rpout.lstat(): - if rpout.isdir() and not rpout.listdir(): # rpout is empty dir - if Globals.change_permissions: - rpout.chmod(0700) # just make sure permissions aren't too lax - elif not datadir.lstat() and not force: Log.FatalError( + assert rpout.lstat(), (rpout.path, rpout.lstat()) + if rpout.isdir() and not rpout.listdir(): # rpout is empty dir + if Globals.change_permissions: + rpout.chmod(0700) # just make sure permissions aren't too lax + elif not Globals.rbdir.lstat() and not force: Log.FatalError( """Destination directory %s @@ -301,17 +286,12 @@ rdiff-backup like this could mess up what is currently in it. If you want to update or overwrite it, run rdiff-backup with the --force option.""" % rpout.path) - if not rpout.lstat(): - try: rpout.mkdir() - except os.error: - Log.FatalError("Unable to create directory %s" % rpout.path) - if not datadir.lstat(): datadir.mkdir() - inc_base = datadir.append_path("increments") + if not Globals.rbdir.lstat(): Globals.rbdir.mkdir() + inc_base = Globals.rbdir.append_path("increments") if not inc_base.lstat(): inc_base.mkdir() if Log.verbosity > 0: - Log.open_logfile(datadir.append("backup.log")) + Log.open_logfile(Globals.rbdir.append("backup.log")) ErrorLog.open(Time.curtimestr, compress = Globals.compression) - backup_warn_if_infinite_regress(rpin, rpout) def backup_warn_if_infinite_regress(rpin, rpout): """Warn user if destination area contained in source area""" @@ -336,6 +316,25 @@ def backup_get_mirrortime(): if mirror_rps: return mirror_rps[0].getinctime() else: return None +def backup_set_fs_globals(rpin, rpout): + """Use fs_abilities to set the globals that depend on filesystem""" + src_fsa = fs_abilities.FSAbilities().init_readonly(rpin) + SetConnections.UpdateGlobal('read_acls', src_fsa.acls) + if src_fsa.eas: rpin.get_ea() + SetConnections.UpdateGlobal('read_eas', src_fsa.eas) + + dest_fsa = fs_abilities.FSAbilities().init_readwrite( + Globals.rbdir, override_chars_to_quote = Globals.chars_to_quote) + SetConnections.UpdateGlobal('preserve_hardlinks', dest_fsa.hardlinks) + SetConnections.UpdateGlobal('fsync_directories', dest_fsa.fsync_dirs) + SetConnections.UpdateGlobal('write_acls', dest_fsa.acls) + SetConnections.UpdateGlobal('write_eas', Globals.read_eas and dest_fsa.eas) + SetConnections.UpdateGlobal('change_ownership', dest_fsa.ownership) + SetConnections.UpdateGlobal('chars_to_quote', dest_fsa.chars_to_quote) + if Globals.chars_to_quote: + for conn in Globals.connections: + conn.FilenameMapping.set_init_quote_vals() + def backup_touch_curmirror_local(rpin, rpout): """Make a file like current_mirror.time.data to record time @@ -367,40 +366,56 @@ def backup_remove_curmirror_local(): older_inc.delete() -def Restore(src_rp, dest_rp = None): +def Restore(src_rp, dest_rp, restore_as_of = None): """Main restoring function - Here src_rp should be an increment file, and if dest_rp is - missing it defaults to the base of the increment. + Here src_rp should be the source file (either an increment or + mirror file), dest_rp should be the target rp to be written. """ - rpin, rpout = restore_check_paths(src_rp, dest_rp) - restore_common(rpin, rpout, rpin.getinctime()) - -def RestoreAsOf(rpin, target): - """Secondary syntax for restore operation - - rpin - RPath of mirror file to restore (not nec. with correct index) - target - RPath of place to put restored file - - """ - rpin, rpout = restore_check_paths(rpin, target, 1) - try: time = Time.genstrtotime(restore_timestr) - except Time.TimeException, exc: Log.FatalError(str(exc)) - restore_common(rpin, target, time) - -def restore_common(rpin, target, time): - """Restore operation common to Restore and RestoreAsOf""" - if target.conn.os.getuid() == 0: - SetConnections.UpdateGlobal('change_ownership', 1) - mirror_root, index = restore_get_root(rpin) - restore_check_backup_dir(mirror_root) - mirror = mirror_root.new_index(index) - inc_rpath = datadir.append_path('increments', index) - restore_set_select(mirror_root, target) - restore_start_log(rpin, target, time) - restore.Restore(mirror, inc_rpath, target, time) - Log("Restore ended", 4) + if not restore_root_set: assert restore_get_root(src_rp) + restore_check_paths(src_rp, dest_rp, restore_as_of) + restore_set_fs_globals(Globals.rbdir) + src_rp = restore_init_quoting(src_rp) + restore_check_backup_dir(restore_root, src_rp, restore_as_of) + if restore_as_of: + try: time = Time.genstrtotime(restore_timestr) + except Time.TimeException, exc: Log.FatalError(str(exc)) + else: time = src_rp.getinctime() + inc_rpath = Globals.rbdir.append_path('increments', restore_index) + restore_set_select(restore_root, dest_rp) + restore_start_log(src_rp, dest_rp, time) + restore.Restore(restore_root.new_index(restore_index), + inc_rpath, dest_rp, time) + Log("Restore finished", 4) + +def restore_init_quoting(src_rp): + """Change rpaths into quoted versions of themselves if necessary""" + global restore_root + if not Globals.chars_to_quote: return src_rp + for conn in Globals.connections: conn.FilenameMapping.set_init_quote_vals() + restore_root = FilenameMapping.get_quotedrpath(restore_root) + SetConnections.UpdateGlobal( + 'rbdir', FilenameMapping.get_quotedrpath(Globals.rbdir)) + return FilenameMapping.get_quotedrpath(src_rp) + +def restore_set_fs_globals(target): + """Use fs_abilities to set the globals that depend on filesystem""" + target_fsa = fs_abilities.FSAbilities().init_readwrite(target, 0) + SetConnections.UpdateGlobal('read_acls', target_fsa.acls) + SetConnections.UpdateGlobal('write_acls', target_fsa.acls) + SetConnections.UpdateGlobal('read_eas', target_fsa.eas) + SetConnections.UpdateGlobal('write_eas', target_fsa.eas) + if Globals.read_eas: target.get_ea() + SetConnections.UpdateGlobal('preserve_hardlinks', target_fsa.hardlinks) + SetConnections.UpdateGlobal('change_ownership', target_fsa.ownership) + + mirror_fsa = fs_abilities.FSAbilities().init_readwrite(Globals.rbdir) + if Globals.chars_to_quote is None: # otherwise already overridden + if mirror_fsa.chars_to_quote: + SetConnections.UpdateGlobal('chars_to_quote', + mirror_fsa.chars_to_quote) + else: SetConnections.UpdateGlobal('chars_to_quote', "") def restore_set_select(mirror_rp, target): """Set the selection iterator on mirror side from command line args @@ -416,7 +431,7 @@ def restore_set_select(mirror_rp, target): def restore_start_log(rpin, target, time): """Open restore log file, log initial message""" - try: Log.open_logfile(datadir.append("restore.log")) + try: Log.open_logfile(Globals.rbdir.append("restore.log")) except LoggerError, e: Log("Warning, " + str(e), 2) # Log following message at file verbosity 3, but term verbosity 4 @@ -430,34 +445,29 @@ def restore_check_paths(rpin, rpout, restoreasof = None): if not restoreasof: if not rpin.lstat(): Log.FatalError("Source file %s does not exist" % rpin.path) - if Globals.quoting_enabled: - rpin = FilenameMapping.get_quotedrpath(rpin, 1) - if not rpin.isincfile(): - Log.FatalError("""File %s does not look like an increment file. - -Try restoring from an increment file (the filenames look like -"foobar.2001-09-01T04:49:04-07:00.diff").""" % rpin.path) - - if not rpout: rpout = rpath.RPath(Globals.local_connection, - rpin.getincbase_str()) - if rpout.lstat() and not force: + if not force and rpout.lstat() and (not rpout.isdir() or rpout.listdir()): Log.FatalError("Restore target %s already exists, " "specify --force to overwrite." % rpout.path) - return rpin, rpout -def restore_check_backup_dir(rpin): +def restore_check_backup_dir(mirror_root, src_rp, restore_as_of): """Make sure backup dir root rpin is in consistent state""" - result = checkdest_need_check(rpin) + if not restore_as_of and not src_rp.isincfile(): + Log.FatalError("""File %s does not look like an increment file. + +Try restoring from an increment file (the filenames look like +"foobar.2001-09-01T04:49:04-07:00.diff").""" % src_rp.path) + + result = checkdest_need_check(mirror_root) if result is None: Log.FatalError("%s does not appear to be an rdiff-backup directory." - % (rpin.path,)) + % (Globals.rbdir.path,)) elif result == 1: Log.FatalError( - "Previous backup to %s seems to have failed." - "Rerun rdiff-backup with --check-destination-dir option to revert" - "directory to state before unsuccessful session." % (rpin.path,)) + "Previous backup to %s seems to have failed.\nRerun rdiff-backup " + "rdiff-with --check-destination-dir option to revert directory " + "to state before unsuccessful session." % (mirror_root.path,)) def restore_get_root(rpin): - """Return (mirror root, index) and set the data dir + """Set data dir, restore_root and index, or return None if fail The idea here is to keep backing up on the path until we find a directory that contains "rdiff-backup-data". That is the @@ -470,7 +480,7 @@ def restore_get_root(rpin): funny way, using symlinks or somesuch. """ - global datadir + global restore_root, restore_index if rpin.isincfile(): relpath = rpin.getincbase().path else: relpath = rpin.path pathcomps = os.path.join(rpin.conn.os.getcwd(), relpath).split("/") @@ -482,23 +492,25 @@ def restore_get_root(rpin): if (parent_dir.isdir() and "rdiff-backup-data" in parent_dir.listdir()): break i = i-1 - else: Log.FatalError("Unable to find rdiff-backup-data directory") - - if not Globals.quoting_enabled: rootrp = parent_dir - else: rootrp = FilenameMapping.get_quotedrpath(parent_dir) - Log("Using mirror root directory %s" % rootrp.path, 6) + else: return None - datadir = rootrp.append_path("rdiff-backup-data") - SetConnections.UpdateGlobal('rbdir', datadir) - if not datadir.isdir(): + restore_root = parent_dir + Log("Using mirror root directory %s" % restore_root.path, 6) + SetConnections.UpdateGlobal('rbdir', + restore_root.append_path("rdiff-backup-data")) + if not Globals.rbdir.isdir(): Log.FatalError("Unable to read rdiff-backup-data directory %s" % - datadir.path) + Globals.rbdir.path) from_datadir = tuple(pathcomps[i:]) if not from_datadir or from_datadir[0] != "rdiff-backup-data": - return (rootrp, from_datadir) # in mirror, not increments - assert from_datadir[1] == "increments" - return (rootrp, from_datadir[2:]) + restore_index = from_datadir # in mirror, not increments + else: + assert (from_datadir[1] == "increments" or + (len(from_datadir) == 2 and + from_datadir[1].startswith('increments'))), from_datadir + restore_index = from_datadir[2:] + return 1 def ListIncrements(rp): @@ -555,7 +567,7 @@ def rom_check_dir(rootrp): rootrp.append_path("rdiff-backup-data")) if not Globals.rbdir.isdir(): Log.FatalError("Unable to open rdiff-backup-data dir %s" % - (datadir.path,)) + (Globals.rbdir.path,)) checkdest_if_necessary(rootrp) @@ -597,6 +609,9 @@ def CheckDest(dest_rp): def checkdest_need_check(dest_rp): """Return None if no dest dir found, 1 if dest dir needs check, 0 o/w""" if not dest_rp.isdir() or not Globals.rbdir.isdir(): return None + if Globals.rbdir.listdir() == ['chars_to_quote']: + # This may happen the first backup just after we test for quoting + return None curmirroot = Globals.rbdir.append("current_mirror") curmir_incs = restore.get_inclist(curmirroot) if not curmir_incs: diff --git a/rdiff-backup/rdiff_backup/Security.py b/rdiff-backup/rdiff_backup/Security.py index 83ddcf2..427f826 100644 --- a/rdiff-backup/rdiff_backup/Security.py +++ b/rdiff-backup/rdiff_backup/Security.py @@ -76,8 +76,9 @@ def set_security_level(action, cmdpairs): rdir = tempfile.gettempdir() elif islocal(cp1): sec_level = "read-only" - rdir = Main.restore_get_root(rpath.RPath(Globals.local_connection, - getpath(cp1)))[0].path + Main.restore_get_root(rpath.RPath(Globals.local_connection, + getpath(cp1))) + rdir = Main.restore_root.path else: assert islocal(cp2) sec_level = "all" diff --git a/rdiff-backup/rdiff_backup/SetConnections.py b/rdiff-backup/rdiff_backup/SetConnections.py index e5d081e..6846953 100644 --- a/rdiff-backup/rdiff_backup/SetConnections.py +++ b/rdiff-backup/rdiff_backup/SetConnections.py @@ -1,4 +1,4 @@ -# Copyright 2002 Ben Escoto +# Copyright 2002, 2003 Ben Escoto # # This file is part of rdiff-backup. # @@ -27,7 +27,7 @@ the related connections. import os from log import Log -import Globals, FilenameMapping, connection, rpath +import Globals, connection, rpath # This is the schema that determines how rdiff-backup will open a # pipe to the remote system. If the file is given as A::B, %s will @@ -178,7 +178,6 @@ def init_connection_settings(conn): conn.log.Log.setterm_verbosity(Log.term_verbosity) for setting_name in Globals.changed_settings: conn.Globals.set(setting_name, Globals.get(setting_name)) - FilenameMapping.set_init_quote_vals() def init_connection_remote(conn_number): """Run on server side to tell self that have given conn_number""" @@ -203,8 +202,6 @@ def BackupInitConnections(reading_conn, writing_conn): writing_conn.Globals.set("isbackup_writer", 1) UpdateGlobal("backup_reader", reading_conn) UpdateGlobal("backup_writer", writing_conn) - if writing_conn.os.getuid() == 0 and Globals.change_ownership != 0: - UpdateGlobal('change_ownership', 1) def CloseConnections(): """Close all connections. Run by client""" diff --git a/rdiff-backup/rdiff_backup/backup.py b/rdiff-backup/rdiff_backup/backup.py index 89d7bea..0be0702 100644 --- a/rdiff-backup/rdiff_backup/backup.py +++ b/rdiff-backup/rdiff_backup/backup.py @@ -1,4 +1,4 @@ -# Copyright 2002 Ben Escoto +# Copyright 2002, 2003 Ben Escoto # # This file is part of rdiff-backup. # @@ -22,7 +22,8 @@ from __future__ import generators import errno import Globals, metadata, rorpiter, TempFile, Hardlink, robust, increment, \ - rpath, static, log, selection, Time, Rdiff, statistics, iterfile + rpath, static, log, selection, Time, Rdiff, statistics, iterfile, \ + eas_acls def Mirror(src_rpath, dest_rpath): """Turn dest_rpath into a copy of src_rpath""" @@ -122,16 +123,27 @@ class DestinationStruct: destination except rdiff-backup-data directory. """ - if use_metadata: - metadata_iter = metadata.GetMetadata_at_time(Globals.rbdir, - Time.prevtime) + def get_basic_iter(): + """Returns iterator of basic metadata""" + metadata_iter = metadata.MetadataFile.get_objects_at_time( + Globals.rbdir, Time.prevtime) if metadata_iter: return metadata_iter log.Log("Warning: Metadata file not found.\n" "Metadata will be read from filesystem.", 2) - sel = selection.Select(rpath) - sel.parse_rbdir_exclude() - return sel.set_iter() + def get_iter_from_fs(): + """Get the combined iterator from the filesystem""" + sel = selection.Select(rpath) + sel.parse_rbdir_exclude() + return sel.set_iter() + + if use_metadata: + if Globals.read_eas: + rorp_iter = eas_acls.ExtendedAttributesFile.\ + get_combined_iter_at_time(Globals.rbdir, Time.prevtime) + else: rorp_iter = get_basic_iter() + if rorp_iter: return rorp_iter + return get_iter_from_fs() def set_rorp_cache(cls, baserp, source_iter, for_increment): """Initialize cls.CCPP, the destination rorp cache @@ -243,7 +255,8 @@ class CacheCollatedPostProcess: self.cache_size = cache_size self.statfileobj = statistics.init_statfileobj() if Globals.file_statistics: statistics.FileStats.init() - metadata.OpenMetadata() + metadata.MetadataFile.open_file() + if Globals.read_eas: eas_acls.ExtendedAttributesFile.open_file() # the following should map indicies to lists # [source_rorp, dest_rorp, changed_flag, success_flag, increment] @@ -317,7 +330,10 @@ class CacheCollatedPostProcess: metadata_rorp = source_rorp else: metadata_rorp = None if metadata_rorp and metadata_rorp.lstat(): - metadata.WriteMetadata(metadata_rorp) + metadata.MetadataFile.write_object(metadata_rorp) + if Globals.read_eas and not metadata_rorp.get_ea().empty(): + eas_acls.ExtendedAttributesFile.write_object( + metadata_rorp.get_ea()) if Globals.file_statistics: statistics.FileStats.update(source_rorp, dest_rorp, changed, inc) @@ -359,7 +375,8 @@ class CacheCollatedPostProcess: def close(self): """Process the remaining elements in the cache""" while self.cache_indicies: self.shorten_cache() - metadata.CloseMetadata() + metadata.MetadataFile.close_file() + if Globals.read_eas: eas_acls.ExtendedAttributesFile.close_file() if Globals.print_statistics: statistics.print_active_stats() if Globals.file_statistics: statistics.FileStats.close() statistics.write_active_statfileobj() diff --git a/rdiff-backup/rdiff_backup/connection.py b/rdiff-backup/rdiff_backup/connection.py index 05aef20..06d7e7a 100644 --- a/rdiff-backup/rdiff_backup/connection.py +++ b/rdiff-backup/rdiff_backup/connection.py @@ -22,6 +22,11 @@ from __future__ import generators import types, os, tempfile, cPickle, shutil, traceback, pickle, \ socket, sys, gzip +# The following EA and ACL modules may be used if available +try: import xattr +except ImportError: pass +try: import posix1e +except ImportError: pass class ConnectionError(Exception): pass @@ -513,7 +518,7 @@ class VirtualFile: import Globals, Time, Rdiff, Hardlink, FilenameMapping, C, Security, \ Main, rorpiter, selection, increment, statistics, manage, lazy, \ iterfile, rpath, robust, restore, manage, backup, connection, \ - TempFile, SetConnections, librsync, log, regress + TempFile, SetConnections, librsync, log, regress, fs_abilities Globals.local_connection = LocalConnection() Globals.connections.append(Globals.local_connection) diff --git a/rdiff-backup/rdiff_backup/eas_acls.py b/rdiff-backup/rdiff_backup/eas_acls.py new file mode 100644 index 0000000..748d8bb --- /dev/null +++ b/rdiff-backup/rdiff_backup/eas_acls.py @@ -0,0 +1,185 @@ +# 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.