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