#!/usr/bin/python3 from pathlib import Path import shutil from typing import Any, Generator, Tuple, Union import click def _to_pathlib_path(ctx: click.Context, param: Any, value: str) \ -> Union[Path, Tuple[Path]]: if isinstance(value, tuple): return tuple(Path(v) for v in value) else: return Path(value) def get_directories(directory: Path) -> Generator[Path, None, None]: """Return all directories below given directory in sorted order This returns first all directories of a directory before returning subdirectories of the sorted subdirectories. """ directories = [directory] for d in directories: for p in sorted(d.iterdir()): if p.is_dir(): directories.append(p) yield p def get_files(directory: Path) -> Generator[Path, None, None]: """Traverse the given directory and its sorted subdirectory and yield files See `get_directories()` for a hint to the order of directories traversed. """ if not directory.is_dir(): msg = f"get_files() needs a directory but got {directory}" raise RuntimeError(msg) directories = [directory] for d in directories: for p in sorted(d.iterdir()): if p.is_dir(): directories.append(p) elif p.is_file(): yield p def relative_path(base: Path, p: Path) -> Path: """Return the part of `p` relative to `base` as relative Path""" for a, b in zip(base.parts, p.parts): if a != b: raise ValueError('Base {} is not the base of {}'.format(a, b)) return Path('/'.join(p.parts[len(base.parts):])) @click.command() @click.argument('srces', nargs=-1, type=click.Path(exists=True, file_okay=True, dir_okay=True, resolve_path=True), callback=_to_pathlib_path, required=True) @click.argument('dest', type=click.Path(exists=True, dir_okay=True, file_okay=False, resolve_path=True), callback=_to_pathlib_path, required=True) @click.option('--verbose', is_flag=True) def main(srces: Tuple[Path], dest: Path, verbose: bool) -> None: """Copy `srces` recursively and sorted one by one to `dest This is mainly useful for USB sticks and MP3 players that take the order of files created in the filesystem instead of the order of the sorted names to play files. """ for src in srces: if src.is_file(): # copy this file over directly rel_path = Path(src.name) dest_path = dest / rel_path if verbose: click.echo('copy {} -> {}'.format(src, dest_path)) shutil.copy(src, dest_path) continue if src.is_dir(): dest_path = dest / src.name if verbose: click.echo('mkdir {}'.format(dest_path)) dest_path.mkdir() # create all directories first for p in get_directories(src): rel_path = Path(src.name) / relative_path(src, p) dest_path = dest / rel_path if verbose: click.echo('mkdir {}'.format(dest_path)) dest_path.mkdir() # copy all files over for p in get_files(src): rel_path = Path(src.name) / relative_path(src, p) dest_path = dest / rel_path if verbose: click.echo('copy {} -> {}'.format(p, dest_path)) shutil.copy(p, dest_path) continue click.echo(f"Warning: Not copying {src}. Neither file nor directory") if __name__ == '__main__': import sys sys.exit(main())