112 lines
3.6 KiB
Python
112 lines
3.6 KiB
Python
|
#!/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())
|