#!/usr/bin/env python import os import sys import shutil import platform # copy_dot_hidden_enable = False # delete_if_not_on_src_enable = True CMD_RMTREE = "rm -Rf" CMD_RM = "rm -f" CMD_COMMENT = "# " CMD_CP = "cp" CMD_MKDIR = "mkdir -p" if platform.system() == "Windows": CMD_RMTREE = "rd /S /Q" # /S: Delete all files and subdirectories and the directory itself. # /Q: Do not ask on global wildcard. CMD_RM = "del /Q /F" # /Q: Do not confirm on wildcard. # /F: Force deleting read-only files. CMD_COMMENT = "REM " CMD_CP = "COPY" CMD_MKDIR = "MD" def path_join_all(names): result = names[0] for i in range(1, len(names)): result = os.path.join(result, names[i]) return result def trim_branch(src, dst, dot_hidden=True, verbose=True): ''' Explore dst non-recursively and delete files and subdirectories recursively that are not present on src. Keyword arguments: dot_hidden -- Operate on files and directories even if they are hidden by starting with '.'. ''' for sub_name in os.listdir(dst): src_sub_path = os.path.join(src, sub_name) dst_sub_path = os.path.join(dst, sub_name) if not dot_hidden: if sub_name.startswith("."): continue if not os.path.exists(src_sub_path): if os.path.isfile(dst_sub_path): print("{} \"{}\"...".format(CMD_RM, dst_sub_path)) os.remove(dst_sub_path) else: print("{} \"{}\"...".format(CMD_RMTREE, dst_sub_path)) shutil.rmtree(dst_sub_path) def update_tree(src, dst, level=0, do_trim=False, dot_hidden=False, verbose=True): ''' Creates dst if not present, then copies everything from src to dst recursively. Keyword arguments: do_trim -- Delete files and directories from dst that are not on src. dot_hidden -- Copy files and directories even if hidden by starting with '.'. ''' folder_path = src indent = " "*level if level <= 1: print(indent + CMD_COMMENT + "* synchronizing with \"{}\"" "".format(dst)) if not os.path.isdir(dst): if verbose: print(indent + CMD_MKDIR + " \"{}\"".format(dst)) os.makedirs(dst) else: if do_trim: trim_branch(src, dst, dot_hidden=dot_hidden) if os.path.isdir(folder_path): for sub_name in os.listdir(folder_path): sub_path = os.path.join(folder_path, sub_name) dst_sub_path = os.path.join(dst, sub_name) allow_copy = True if not dot_hidden: allow_copy = not sub_name.startswith(".") if not allow_copy: continue if os.path.isdir(sub_path): update_tree(sub_path, dst_sub_path, level=level+1, do_trim=do_trim, dot_hidden=dot_hidden, verbose=verbose) elif os.path.isfile(sub_path): mode = None sub_mt = os.path.getmtime(sub_path) dst_sub_mt = os.path.getmtime(dst_sub_path) if not os.path.isfile(dst_sub_path): mode = "+" elif sub_mt > dst_sub_mt: mode = ">" elif sub_mt < dst_sub_mt: # mode = "<" # Don't set any mode, or the newer file will be overwritten! print(indent + CMD_COMMENT + "WARNING: \"{}\" is newer on destination!" "".format(dst_sub_path)) if mode is None: continue try: if verbose: if mode == ">": print(indent + CMD_CP + "update:") print(indent + CMD_CP + " \"{}\" \"{}\"" "".format(sub_path, dst_sub_path)) # shutil.copyfile(sub_path, dst_sub_path) shutil.copy2(sub_path, dst_sub_path) except PermissionError: print(indent + CMD_COMMENT + "PermissionError:") print(indent + CMD_COMMENT + " {} \"{}\" \"{}\"" "".format(CMD_CP, sub_path, dst_sub_path)) pass USAGE = ''' Syntax: forwardfilesync.py [options] --hidden Process files & folders even if named starting with '.'. --delete Delete files & folders on the destination if not in source. ''' def usage(): print(USAGE) def main(): flags = {} flags["hidden"] = False flags["delete"] = False if len(sys.argv) < 3: usage() print("Error: You must provide at least a source and destination.") return 1 src = sys.argv[1] dst = sys.argv[2] for argI in range(3, len(sys.argv)): arg = sys.argv[argI] if (arg[:2] != "--"): usage() print("Error: The option \"{}\" is not formatted correctly" " since it doesn't start with \"--\". If it is part" " of a path with spaces, put the path in quotes." "".format(sys.argv[argI])) return 1 name = arg[2:] if name not in flags: usage() print("Error: There is no option \"{}\". If it is part of a" " path with spaces, put the path in quotes." "".format(sys.argv[argI])) return 1 flags[name] = True print(CMD_COMMENT + "Using options:") for k, v in flags.items(): print(CMD_COMMENT + "{}: {}".format(k, v)) update_tree( src, dst, do_trim=flags["delete"] is True, dot_hidden=flags["hidden"] is True, ) print(CMD_COMMENT + "Done.") return 0 if __name__ == "__main__": sys.exit(main())