diff --git a/jlauncher/gitops.py b/jlauncher/gitops.py index ebce0aa..cfddd89 100644 --- a/jlauncher/gitops.py +++ b/jlauncher/gitops.py @@ -10,7 +10,9 @@ import datetime as _dt import json import os import shutil +import stat import subprocess +import sys import urllib.error import urllib.request from collections.abc import Callable @@ -49,6 +51,31 @@ def _noop(_: str) -> None: pass +def _force_rmtree(path: Path, log: LogFn = _noop) -> None: + """Esborra un arbre de fitxers, fins i tot amb fitxers de només-lectura. + + A Windows els objectes de ``.git`` es creen com a només-lectura i fan que + ``shutil.rmtree`` falli amb ``PermissionError``; cal llevar el bit d'escriptura + i reintentar. A diferència de ``ignore_errors=True``, aquí els errors que no + puguem resoldre es registren al log en lloc d'empassar-se en silenci. + """ + if not path.exists(): + return + + def handle(func: Callable, p: str, _exc: object) -> None: + try: + os.chmod(p, stat.S_IWRITE) + func(p) # reintenta l'operació (unlink/rmdir) que havia fallat + except OSError as exc: + log(f"No s'ha pogut esborrar {p}: {exc}") + + # Python 3.12 va renombrar el paràmetre `onerror` a `onexc`; suportem tots dos. + if sys.version_info >= (3, 12): + shutil.rmtree(path, onexc=handle) + else: + shutil.rmtree(path, onerror=handle) + + def _auth_args(token: str) -> list[str]: """Args -c para autenticar git ante Gitea con un token, sin tocar .git/config.""" if not token: @@ -166,7 +193,7 @@ def delete_local(root: Path, game: Game, log: LogFn = _noop) -> None: log(f"{game.name}: no hi ha res a esborrar") return log(f"Esborrant la descàrrega local de {game.name}…") - shutil.rmtree(target, ignore_errors=True) + _force_rmtree(target, log) def download( @@ -201,7 +228,7 @@ def download( else: log(f"Clonant {game.name}…") if repo.exists(): # carpeta a medias sin .git: limpiarla - shutil.rmtree(repo, ignore_errors=True) + _force_rmtree(repo, log) _run_git( ["clone", game.clone_url, str(repo)], None,