Compare commits
2 Commits
a7d5c21c45
...
12c3ac9c05
Author | SHA1 | Date |
---|---|---|
|
12c3ac9c05 | |
|
38fe8c0a82 |
|
@ -114,3 +114,5 @@ dmypy.json
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# VIM
|
||||||
|
*.swp
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
[metadata]
|
||||||
|
name = sorted-copy
|
||||||
|
version = 0.1
|
||||||
|
description = Copy files and directories recursively sorted and one by one to the destination. Mainly useful for USB sticks/MP3 players.
|
||||||
|
|
||||||
|
[options]
|
||||||
|
scripts = sorted-co.py
|
||||||
|
install_requires =
|
||||||
|
click
|
||||||
|
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
||||||
|
exclude = .git,__pycache__,*.egg-info,*lib/python*
|
||||||
|
ignore = E241,E741,W503,W504
|
|
@ -0,0 +1,4 @@
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
|
||||||
|
setuptools.setup()
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/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())
|
Loading…
Reference in New Issue