sff
1 #!/usr/bin/env python3 2 3 import argparse 4 import os 5 import re 6 import shutil 7 from argparse import FileType 8 from pathlib import Path 9 10 11 def target_dir(dir_): 12 dir_ = Path(dir_) 13 if not dir_.is_dir(): 14 raise argparse.ArgumentTypeError("Please pass a valid path to a directory") 15 return dir_ 16 17 18 def list_dir(dir_, ignore_regex=None): 19 tree_items = [] 20 for (dirpath, dirnames, filenames) in os.walk(dir_): 21 dirpath = Path(dirpath) 22 rel_dirpath = dirpath.relative_to(dir_) 23 for item in filenames: 24 rel_path = str(rel_dirpath / item) 25 if ignore_regex and ignore_regex.search(rel_path): 26 continue 27 tree_items.append(rel_path) 28 return tree_items 29 30 31 def get_abs_and_rel(parent, path): 32 if path.is_absolute(): 33 if str(path).startswith(str(parent)): 34 rel_path = path.relative_to(parent) 35 else: 36 rel_path = path 37 return path, rel_path 38 else: 39 abs_path = parent / path 40 return abs_path, path 41 42 43 def main(args): 44 parent = Path(args.parent or os.curdir).resolve() 45 46 dry_run = args.dry_run 47 48 def p(string): 49 if dry_run: 50 string = "would %s" % string 51 print(string) 52 53 relative_sources = set() 54 ignored_sources = {} 55 readlines = args.file.readlines() 56 print("lines read: %s" % len(readlines)) 57 target_dir = args.target_dir 58 for path_str in readlines: 59 path_str = path_str.strip() 60 # Ignore comments 61 if path_str.strip().startswith("#"): 62 continue 63 abs_path, rel_path = get_abs_and_rel(parent, Path(path_str)) 64 if not abs_path.exists(): 65 print("'%s' doesn't exist" % abs_path) 66 ignored_sources[str(abs_path)] = "Nonexistent" 67 continue 68 69 if args.recreate_dirs: 70 target_path = target_dir / rel_path 71 else: 72 target_path = target_dir / rel_path.name 73 74 if target_path.exists(): 75 source_stat = abs_path.stat() 76 target_stat = target_path.stat() 77 # Don't touch unchanged files 78 if source_stat.st_size == target_stat.st_size: 79 ignored_sources[str(rel_path)] = "Same size" 80 continue 81 elif not dry_run: 82 target_path.parent.mkdir(parents=True, exist_ok=True) 83 84 p("Copy %s" % rel_path) 85 if not dry_run: 86 shutil.copy(abs_path, target_path) 87 88 relative_sources.add(str(target_path.relative_to(target_dir))) 89 90 print("%s ignored" % len(ignored_sources)) 91 print("%s to copy" % len(relative_sources)) 92 93 # Remove files only on destination 94 destination_items = set(list_dir(target_dir, args.dest_exclude)) 95 destination_items_len = len(destination_items) 96 print("%s files in destination" % destination_items_len) 97 98 ignored_set = set(ignored_sources.keys()) 99 files_to_delete = (destination_items - relative_sources) - ignored_set 100 to_delete_len = len(files_to_delete) 101 print("expected count: %s" % (destination_items_len - to_delete_len,)) 102 103 if to_delete_len: 104 print("%s to delete" % to_delete_len) 105 for to_delete in files_to_delete: 106 to_delete = target_dir / to_delete 107 p("delete %s" % to_delete) 108 if not dry_run: 109 to_delete.unlink() 110 else: 111 print("Nothing to delete") 112 print("Done!") 113 114 115 if __name__ == '__main__': 116 parser = argparse.ArgumentParser( 117 description= 118 "Sync from file." 119 "Reads a list of files and directories to hard-sync to a directory. " 120 "Any file or directory not in the target dir will be deleted!") 121 122 # parser.add_argument("--ignore-mod-date", 123 # help="Ignores modification date and " 124 # "only checks if target exists") 125 parser.add_argument("-p", "--parent", help="Parent of the paths in the directory." 126 "If the paths are relative, this will serve as their parent" 127 "If the paths are absolute, this will strip off the parent") 128 parser.add_argument("-c", "--recreate-dirs", 129 help="Recreate parent directories of paths in given file " 130 "otherwise the files will be directly copied into the target directory", 131 action="store_true") 132 # TODO: Exclude pattern for source files 133 # parser.add_argument("-e", "--exclude", 134 # help="Exclude regex on source", type=re.compile) 135 parser.add_argument("-E", "--dest-exclude", 136 help="Exclude regex on destination", type=re.compile) 137 138 parser.add_argument("-n", "--dry-run", action="store_true") 139 parser.add_argument("file", type=FileType()) 140 parser.add_argument("target_dir", type=target_dir) 141 142 main(parser.parse_args())