-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmerge-dirs.py
84 lines (64 loc) · 2.5 KB
/
merge-dirs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import errno
import logging
import os
from typing import Callable, Optional
from genutility.exceptions import assert_choice
def remove_empty_error_log(path, e):
if e.errno != errno.ENOTEMPTY:
logging.warning("Failed to remove %s (%s)", path, e)
def remove_empty_dirs(path: str, ignore_errors: bool = False, onerror: Optional[Callable] = None) -> None:
for dirpath, _dirnames, filenames in os.walk(path, topdown=False):
if filenames:
continue # skip remove
try:
os.rmdir(dirpath)
except OSError as e:
if ignore_errors:
pass
elif onerror:
onerror(dirpath, e)
else:
raise
def move_rename(src, dst):
raise RuntimeError("Not implemented")
MODES = ("fail", "no_move", "overwrite", "rename")
def merge(src, dst, mode="no_move"):
assert_choice("mode", mode, MODES)
if not src.is_dir() or not dst.is_dir():
raise ValueError("src and dst must be directories")
for path in src.rglob("*"):
if path.is_dir():
relpath = path.relative_to(src)
(dst / relpath).mkdir(parents=True, exist_ok=True)
elif path.is_file():
relpath = path.relative_to(src)
(dst / relpath.parent).mkdir(parents=True, exist_ok=True)
target = dst / relpath
if target.exists():
if mode == "fail":
raise FileExistsError(os.fspath(path))
elif mode == "no_move":
pass
elif mode == "overwrite":
os.replace(os.fspath(path), os.fspath(target))
elif mode == "rename":
move_rename(path, target)
else:
path.rename(target) # race condition on linux, us renameat2 with RENAME_NOREPLACE?
else:
raise RuntimeError(f"Unhandled file: {path}")
remove_empty_dirs(os.fspath(src), onerror=remove_empty_error_log)
if __name__ == "__main__":
from argparse import ArgumentParser
from genutility.args import is_dir
parser = ArgumentParser()
parser.add_argument("src", type=is_dir, help="Source directory")
parser.add_argument("dst", type=is_dir, help="Target directory")
parser.add_argument(
"--mode",
choices=MODES,
default="no_move",
help="Specifies the handling of files in src which already exist in dst.",
)
args = parser.parse_args()
merge(args.src, args.dst, args.mode)