|
1 | | -from pathlib import Path as _Path |
2 | | -import shutil as _shutil |
| 1 | +from pathlib import Path |
| 2 | +import shutil |
| 3 | +import os |
3 | 4 |
|
4 | | -from fileex import exception as _exception |
| 5 | +from fileex import exception |
5 | 6 |
|
| 7 | +__all__ = ["delete_contents"] |
6 | 8 |
|
7 | | -def delete_contents( |
8 | | - path: str | _Path, exclude: list[str] | None = None, raise_existence: bool = True |
9 | | -) -> list[str] | None: |
10 | | - """ |
11 | | - Delete all files and directories within a given directory, |
12 | | - excluding those specified by `exclude`. |
| 9 | + |
| 10 | +def delete( |
| 11 | + path: str | Path, |
| 12 | + exclude: list[str] | None = None, |
| 13 | + raise_existence: bool = True |
| 14 | +) -> tuple[list[Path], list[Path]] | tuple[None, None]: |
| 15 | + """Recursively delete files and directories within a given directory, excluding those specified by `exclude`. |
13 | 16 |
|
14 | 17 | Parameters |
15 | 18 | ---------- |
16 | | - path : str | pathlib.Path |
| 19 | + path |
17 | 20 | Path to the directory whose content should be deleted. |
18 | | - exclude : list[str] | None, default: None |
19 | | - List of file and directory names to exclude from deletion. |
| 21 | + exclude |
| 22 | + List of glob patterns to exclude from deletion. |
| 23 | + Patterns are relative to the directory specified by `path`. |
| 24 | + If a directory is excluded, all its contents will also be excluded. |
20 | 25 | raise_existence : bool, default: True |
21 | 26 | Raise an error when the directory does not exist. |
22 | 27 |
|
23 | 28 | Returns |
24 | 29 | ------- |
25 | | - deleted_names : list[str] | None |
26 | | - Names of the files and directories that were deleted, |
| 30 | + Paths of the files and directories that were deleted and excluded, respectively, |
27 | 31 | or None if the directory does not exist and `raise_existence` is set to False. |
28 | 32 |
|
29 | 33 | Raises |
30 | 34 | ------ |
31 | 35 | fileex.exception.FileExPathNotFoundError |
32 | 36 | If the directory does not exist and `raise_existence` is set to True. |
33 | 37 | """ |
34 | | - path = _Path(path) |
| 38 | + path = Path(path).resolve() |
35 | 39 | if not path.is_dir(): |
36 | 40 | if raise_existence: |
37 | | - raise _exception.FileExPathNotFoundError(path, is_dir=True) |
38 | | - return |
39 | | - if not exclude: |
40 | | - exclude = [] |
41 | | - deleted_names = [] |
42 | | - for item in path.iterdir(): |
43 | | - if item.name not in exclude: |
44 | | - deleted_names.append(item.name) |
45 | | - item.unlink() if item.is_file() else _shutil.rmtree(item) |
46 | | - return deleted_names |
| 41 | + raise exception.FileExPathNotFoundError(path, is_dir=True) |
| 42 | + return None, None |
| 43 | + |
| 44 | + excluded_paths = set() |
| 45 | + for pattern in exclude or []: |
| 46 | + for excluded_path in path.glob(pattern): |
| 47 | + excluded_paths.add(excluded_path) |
| 48 | + if excluded_path.is_dir(): |
| 49 | + # Also exclude all contents of the directory |
| 50 | + excluded_paths.update(excluded_path.rglob("*")) |
| 51 | + |
| 52 | + deleted = [] |
| 53 | + # Walk bottom-up so we can delete files before trying to delete directories |
| 54 | + for current_dir, dirs, files in os.walk(path, topdown=False): |
| 55 | + current_path = Path(current_dir) |
| 56 | + # Delete files |
| 57 | + for name in files: |
| 58 | + file_path = current_path / name |
| 59 | + if file_path.resolve() in excluded_paths: |
| 60 | + continue |
| 61 | + file_path.unlink() |
| 62 | + deleted.append(file_path) |
| 63 | + # Delete directories |
| 64 | + for name in dirs: |
| 65 | + dir_path = current_path / name |
| 66 | + if dir_path.resolve() in excluded_paths: |
| 67 | + continue |
| 68 | + shutil.rmtree(dir_path) |
| 69 | + deleted.append(dir_path) |
| 70 | + return deleted, list(excluded_paths) |
| 71 | + |
| 72 | + |
| 73 | +def merge( |
| 74 | + source: str | Path, |
| 75 | + destination: str | Path, |
| 76 | + raise_existence: bool = True |
| 77 | +) -> list[Path] | None: |
| 78 | + """Recursively merge a directory into another. |
| 79 | +
|
| 80 | + All files and subdirectories in `source` will be moved to `destination`. |
| 81 | + Existing files in `destination` will be overwritten. |
| 82 | +
|
| 83 | + Parameters |
| 84 | + ---------- |
| 85 | + source |
| 86 | + Path to the source directory. |
| 87 | + destination |
| 88 | + Path to the destination directory. |
| 89 | + raise_existence |
| 90 | + Raise an error if the source directory does not exist. |
| 91 | +
|
| 92 | + Returns |
| 93 | + ------- |
| 94 | + Paths of the files and directories that were moved, |
| 95 | + or None if the source directory does not exist and `raise_existence` is set to False. |
| 96 | +
|
| 97 | + Raises |
| 98 | + ------ |
| 99 | + fileex.exception.FileExPathNotFoundError |
| 100 | + If the source directory does not exist and `raise_existence` is set to True. |
| 101 | + """ |
| 102 | + source = Path(source).resolve() |
| 103 | + destination = Path(destination).resolve() |
| 104 | + moved_paths = [] |
| 105 | + if not source.is_dir(): |
| 106 | + if raise_existence: |
| 107 | + raise exception.FileExPathNotFoundError(source, is_dir=True) |
| 108 | + return None, None |
| 109 | + for item in source.iterdir(): |
| 110 | + dest_item = destination / item.name |
| 111 | + if item.is_dir(): |
| 112 | + if dest_item.exists(): |
| 113 | + # Merge the subdirectory |
| 114 | + move_and_merge(item, dest_item) |
| 115 | + else: |
| 116 | + # Move the whole directory |
| 117 | + shutil.move(str(item), str(dest_item)) |
| 118 | + moved_paths.append(dest_item) |
| 119 | + else: |
| 120 | + # Move or overwrite the file |
| 121 | + if dest_item.exists(): |
| 122 | + # Remove the existing file |
| 123 | + dest_item.unlink() |
| 124 | + shutil.move(str(item), str(dest_item)) |
| 125 | + moved_paths.append(dest_item) |
| 126 | + source.rmdir() |
| 127 | + return moved_paths |
0 commit comments