• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Mirrors a directory tree to another directory using hard links."""
15
16import argparse
17import os
18from pathlib import Path
19from typing import Iterable, Iterator, List
20
21
22def _parse_args() -> argparse.Namespace:
23    """Registers the script's arguments on an argument parser."""
24
25    parser = argparse.ArgumentParser(description=__doc__)
26
27    parser.add_argument('--source-root',
28                        type=Path,
29                        required=True,
30                        help='Prefix to strip from the source files')
31    parser.add_argument('sources',
32                        type=Path,
33                        nargs='*',
34                        help='Files to mirror to the directory')
35    parser.add_argument('--directory',
36                        type=Path,
37                        required=True,
38                        help='Directory to which to mirror the sources')
39    parser.add_argument('--path-file',
40                        type=Path,
41                        help='File with paths to files to mirror')
42
43    return parser.parse_args()
44
45
46def _link_files(source_root: Path, sources: Iterable[Path],
47                directory: Path) -> Iterator[Path]:
48    for source in sources:
49        dest = directory / source.relative_to(source_root)
50        dest.parent.mkdir(parents=True, exist_ok=True)
51
52        if dest.exists():
53            dest.unlink()
54
55        # Use a hard link to avoid unnecessary copies. Resolve the source before
56        # linking in case it is a symlink.
57        os.link(source.resolve(), dest)
58
59        yield dest
60
61
62def _link_files_or_dirs(paths: Iterable[Path],
63                        directory: Path) -> Iterator[Path]:
64    """Links files or directories into the output directory.
65
66    Files are linked directly; files in directories are linked as relative paths
67    from the directory.
68    """
69
70    for path in paths:
71        if path.is_dir():
72            files = (p for p in path.glob('**/*') if p.is_file())
73            yield from _link_files(path, files, directory)
74        elif path.is_file():
75            yield from _link_files(path.parent, [path], directory)
76        else:
77            raise FileNotFoundError(f'{path} does not exist!')
78
79
80def mirror_paths(source_root: Path,
81                 sources: Iterable[Path],
82                 directory: Path,
83                 path_file: Path = None) -> List[Path]:
84    """Creates hard links in the provided directory for the provided sources.
85
86    Args:
87      source_root: Base path for files in sources.
88      sources: Files to link to from the directory.
89      directory: The output directory.
90      path_file: A file with file or directory paths to link to.
91    """
92    directory.mkdir(parents=True, exist_ok=True)
93
94    outputs = list(_link_files(source_root, sources, directory))
95
96    if path_file:
97        paths = (Path(p).resolve() for p in path_file.read_text().splitlines())
98        outputs.extend(_link_files_or_dirs(paths, directory))
99
100    return outputs
101
102
103if __name__ == '__main__':
104    mirror_paths(**vars(_parse_args()))
105