diff options
Diffstat (limited to 'rdiff-backup/rdiff_backup/Main.py')
-rw-r--r-- | rdiff-backup/rdiff_backup/Main.py | 327 |
1 files changed, 171 insertions, 156 deletions
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: |