151 lines
4.7 KiB
Python
151 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
def existing_input_paths(args: list[str], *, print_missing: bool = False) -> list[Path]:
|
|
paths: list[Path] = []
|
|
for arg in args:
|
|
path = Path(arg).expanduser()
|
|
if path.exists():
|
|
paths.append(path.resolve())
|
|
elif print_missing:
|
|
print(f"Skipping missing path: {arg}")
|
|
return paths
|
|
|
|
|
|
def determine_working_dir(args: list[str]) -> Path:
|
|
paths = existing_input_paths(args)
|
|
if not paths:
|
|
raise RuntimeError("No existing input paths were provided.")
|
|
if len(paths) == 1:
|
|
path = paths[0]
|
|
return path if path.is_dir() else path.parent
|
|
|
|
try:
|
|
common = Path(os.path.commonpath([str(path) for path in paths]))
|
|
except ValueError as exc:
|
|
raise RuntimeError("Input paths do not have a common ancestor.") from exc
|
|
|
|
if str(common) == common.anchor:
|
|
raise RuntimeError("Input paths only share the filesystem root; choose items closer together.")
|
|
return common
|
|
|
|
|
|
def collect_files(
|
|
args: list[str],
|
|
*,
|
|
allowed_exts: set[str] | None = None,
|
|
print_missing: bool = False,
|
|
sort_by_name: bool = False,
|
|
) -> list[Path]:
|
|
files: list[Path] = []
|
|
seen: set[Path] = set()
|
|
|
|
for arg in args:
|
|
path = Path(arg).expanduser()
|
|
if path.is_file():
|
|
candidates = [path] if allowed_exts is None or path.suffix.lower() in allowed_exts else []
|
|
elif path.is_dir():
|
|
candidates = [
|
|
child
|
|
for child in path.rglob("*")
|
|
if child.is_file()
|
|
and (allowed_exts is None or child.suffix.lower() in allowed_exts)
|
|
]
|
|
else:
|
|
if print_missing:
|
|
print(f"Skipping missing path: {arg}")
|
|
continue
|
|
|
|
for candidate in candidates:
|
|
resolved = candidate.resolve()
|
|
if resolved not in seen:
|
|
seen.add(resolved)
|
|
files.append(resolved)
|
|
|
|
if sort_by_name:
|
|
return sorted(files, key=lambda path: path.name.lower())
|
|
return sorted(files, key=lambda path: str(path).lower())
|
|
|
|
|
|
def first_file_from_path(path: Path) -> Path | None:
|
|
path = path.expanduser()
|
|
if path.is_file():
|
|
return path.resolve()
|
|
if path.is_dir():
|
|
for child in sorted(path.rglob("*"), key=lambda item: str(item).lower()):
|
|
if child.is_file():
|
|
return child.resolve()
|
|
return None
|
|
|
|
|
|
def cleanup_roots_from_args(args: list[str]) -> list[Path]:
|
|
roots: list[Path] = []
|
|
seen: set[Path] = set()
|
|
for arg in args:
|
|
path = Path(arg).expanduser()
|
|
if path.is_dir():
|
|
resolved = path.resolve()
|
|
if resolved not in seen:
|
|
seen.add(resolved)
|
|
roots.append(resolved)
|
|
return roots
|
|
|
|
|
|
def unique_path(path: Path) -> Path:
|
|
if not path.exists():
|
|
return path
|
|
|
|
for index in range(1, 10000):
|
|
candidate = path.with_name(f"{path.stem}-{index}{path.suffix}")
|
|
if not candidate.exists():
|
|
return candidate
|
|
|
|
raise RuntimeError(f"Could not find an available name for {path}")
|
|
|
|
|
|
def unique_existing_target(path: Path, planned_sources: set[Path]) -> Path:
|
|
if not path.exists() or path.resolve() in planned_sources:
|
|
return path
|
|
for index in range(1, 10000):
|
|
candidate = path.with_name(f"{path.stem}-{index}{path.suffix}")
|
|
if not candidate.exists() or candidate.resolve() in planned_sources:
|
|
return candidate
|
|
raise RuntimeError(f"Could not find available target name for {path}")
|
|
|
|
|
|
def remove_empty_dirs(cleanup_roots: list[Path], keep_dirs: set[Path]) -> list[Path]:
|
|
removed: list[Path] = []
|
|
candidates: set[Path] = set()
|
|
for root in cleanup_roots:
|
|
if not root.exists() or not root.is_dir():
|
|
continue
|
|
for directory, subdirs, _files in os.walk(root, topdown=False):
|
|
path = Path(directory).resolve()
|
|
candidates.add(path)
|
|
for subdir in subdirs:
|
|
child = (Path(directory) / subdir).resolve()
|
|
if child.exists() and child.is_dir():
|
|
candidates.add(child)
|
|
|
|
for directory in sorted(candidates, key=lambda path: len(path.parts), reverse=True):
|
|
if directory in keep_dirs:
|
|
continue
|
|
try:
|
|
directory.rmdir()
|
|
removed.append(directory)
|
|
except OSError:
|
|
pass
|
|
|
|
return removed
|
|
|
|
|
|
def display_path_from_working_dir(path: Path, working_dir: Path) -> str:
|
|
try:
|
|
relative = path.resolve().relative_to(working_dir.resolve())
|
|
except ValueError:
|
|
return str(path)
|
|
return "." if str(relative) == "." else str(relative)
|