Restructuration de l'application
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
WINDOWS_RESERVED_NAMES = {
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
*(f"COM{number}" for number in range(1, 10)),
|
||||
*(f"LPT{number}" for number in range(1, 10)),
|
||||
}
|
||||
|
||||
|
||||
class WindowsLabelError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def windows_filename_problems(value: str) -> list[str]:
|
||||
problems = []
|
||||
forbidden = sorted({character for character in value if character in '<>:"/\\|?*'})
|
||||
if forbidden:
|
||||
problems.append("caractères interdits " + " ".join(repr(char) for char in forbidden))
|
||||
if any(ord(character) < 32 for character in value):
|
||||
problems.append("caractère de contrôle")
|
||||
if value.endswith((" ", ".")):
|
||||
problems.append("espace ou point final")
|
||||
if not value or value in {".", ".."}:
|
||||
problems.append("nom vide ou réservé")
|
||||
elif value.split(".", 1)[0].upper() in WINDOWS_RESERVED_NAMES:
|
||||
problems.append("nom système réservé")
|
||||
return problems
|
||||
|
||||
|
||||
def validate_windows_labels(
|
||||
labels: list[str] | tuple[str, ...], platform_name: str | None = None
|
||||
) -> None:
|
||||
"""Reject labels that cannot be used as filenames on native Windows."""
|
||||
if (platform_name or os.name) != "nt":
|
||||
return
|
||||
invalid = []
|
||||
for label in labels:
|
||||
problems = windows_filename_problems(label)
|
||||
if problems:
|
||||
invalid.append((label, problems))
|
||||
if not invalid:
|
||||
return
|
||||
details = "\n".join(
|
||||
f" - {label!r}: {', '.join(problems)}" for label, problems in invalid
|
||||
)
|
||||
raise WindowsLabelError(
|
||||
"Labels incompatibles avec Windows :\n"
|
||||
f"{details}\n"
|
||||
"Modifiez ces labels avant de poursuivre (par exemple, remplacez ':' par ' - ')."
|
||||
)
|
||||
|
||||
|
||||
def safe_filename(value: str, fallback: str = "Unknown") -> str:
|
||||
"""Return a filename component accepted by both Linux and Windows."""
|
||||
cleaned = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "", value).strip().rstrip(". ")
|
||||
if not cleaned:
|
||||
cleaned = fallback
|
||||
if cleaned.split(".", 1)[0].upper() in WINDOWS_RESERVED_NAMES:
|
||||
cleaned = f"_{cleaned}"
|
||||
return cleaned
|
||||
|
||||
|
||||
def open_path(path: str | Path) -> None:
|
||||
"""Open a file with the desktop's default application."""
|
||||
target = str(Path(path).expanduser().resolve())
|
||||
if os.name == "nt":
|
||||
os.startfile(target) # type: ignore[attr-defined]
|
||||
elif sys.platform.startswith("linux"):
|
||||
subprocess.Popen(["xdg-open", target])
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported platform: {sys.platform}")
|
||||
|
||||
|
||||
def launch_pdf_arranger(path: str | Path) -> None:
|
||||
executable = shutil.which("pdf-arranger") or shutil.which("pdfarranger")
|
||||
if not executable:
|
||||
raise FileNotFoundError(
|
||||
"PDF Arranger is not installed or is not available in PATH."
|
||||
)
|
||||
subprocess.Popen([executable, str(Path(path).expanduser().resolve())])
|
||||
|
||||
|
||||
def replace_with_link_or_copy(
|
||||
source: str | Path,
|
||||
destination: str | Path,
|
||||
*,
|
||||
prefer: Literal["hardlink", "symlink"] = "hardlink",
|
||||
) -> str:
|
||||
"""Replace destination with a link, falling back to a regular copy."""
|
||||
source_path = Path(source).expanduser().resolve()
|
||||
destination_path = Path(destination).expanduser()
|
||||
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if os.path.lexists(destination_path):
|
||||
destination_path.unlink()
|
||||
|
||||
strategies = (prefer, "symlink" if prefer == "hardlink" else "hardlink")
|
||||
for strategy in strategies:
|
||||
try:
|
||||
if strategy == "hardlink":
|
||||
os.link(source_path, destination_path)
|
||||
else:
|
||||
os.symlink(source_path, destination_path)
|
||||
return strategy
|
||||
except (NotImplementedError, OSError):
|
||||
continue
|
||||
|
||||
shutil.copy2(source_path, destination_path)
|
||||
return "copy"
|
||||
|
||||
Reference in New Issue
Block a user