Socle de la refonte GUI V6 (couche présentation uniquement, aucune logique de détection) : - license_store: stockage licence hors dépôt (%LOCALAPPDATA%/Aivanov | XDG), read/write atomique/delete, ne journalise aucun token - license_client: LicenseStatus + activate/check/local_status, session HTTP injectable, serveur indisponible géré sans crash, aucune clé privée - theme: 4 thèmes + couleurs de statut licence - app + tab_about: shell customtkinter minimal (header, bandeau licence, 3 onglets), onglet À propos étoffé - Pseudonymisation_Gui_V6.py: point d'entrée + --self-test (exit 0 sans fenêtre) - requirements.txt: customtkinter==5.2.2 Tests: 20 nouveaux (store sur vrais fichiers, client sur session injectée). Suite tests/unit: 167 passed, 0 régression. V5/moteur/managers/specs intacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""Stockage local de la licence signée, hors dépôt.
|
|
|
|
La licence est stockée dans l'espace utilisateur, jamais dans le dépôt Git :
|
|
|
|
- Windows : ``%LOCALAPPDATA%/Aivanov/Anonymisation/license.json``
|
|
- Linux/dev : ``~/.local/share/aivanov/anonymisation/license.json``
|
|
|
|
Règle de sécurité : ce module ne journalise jamais le contenu de la licence
|
|
(le token signé est sensible). Aucune impression, aucun log du payload.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
_APP_VENDOR = "Aivanov"
|
|
_APP_NAME = "Anonymisation"
|
|
_LICENSE_FILENAME = "license.json"
|
|
|
|
|
|
def default_license_path() -> Path:
|
|
"""Retourne le chemin du fichier licence dans l'espace utilisateur.
|
|
|
|
Le répertoire n'est pas créé ici ; il l'est à la première écriture.
|
|
"""
|
|
if sys.platform.startswith("win"):
|
|
base = os.environ.get("LOCALAPPDATA")
|
|
root = Path(base) if base else Path.home() / "AppData" / "Local"
|
|
return root / _APP_VENDOR / _APP_NAME / _LICENSE_FILENAME
|
|
|
|
# Linux / macOS / dev : respecte XDG_DATA_HOME si défini.
|
|
base = os.environ.get("XDG_DATA_HOME")
|
|
root = Path(base) if base else Path.home() / ".local" / "share"
|
|
return root / _APP_VENDOR.lower() / _APP_NAME.lower() / _LICENSE_FILENAME
|
|
|
|
|
|
class LicenseStore:
|
|
"""Lit / écrit / supprime le fichier licence local (JSON).
|
|
|
|
Le chemin est injectable pour les tests (vrai fichier dans un tmp_path) ;
|
|
par défaut il pointe vers l'emplacement utilisateur de la plateforme.
|
|
"""
|
|
|
|
def __init__(self, path: Optional[Path] = None) -> None:
|
|
self._path = Path(path) if path is not None else default_license_path()
|
|
|
|
@property
|
|
def path(self) -> Path:
|
|
return self._path
|
|
|
|
def exists(self) -> bool:
|
|
return self._path.is_file()
|
|
|
|
def load(self) -> Optional[dict[str, Any]]:
|
|
"""Retourne le payload licence, ou ``None`` si absent / illisible."""
|
|
if not self._path.is_file():
|
|
return None
|
|
try:
|
|
with self._path.open("r", encoding="utf-8") as handle:
|
|
data = json.load(handle)
|
|
except (json.JSONDecodeError, OSError, ValueError):
|
|
return None
|
|
return data if isinstance(data, dict) else None
|
|
|
|
def save(self, data: dict[str, Any]) -> Path:
|
|
"""Écrit le payload licence de façon atomique. Crée le répertoire parent."""
|
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = self._path.with_name(self._path.name + ".tmp")
|
|
with tmp.open("w", encoding="utf-8") as handle:
|
|
json.dump(data, handle, ensure_ascii=False, indent=2)
|
|
os.replace(tmp, self._path)
|
|
return self._path
|
|
|
|
def delete(self) -> bool:
|
|
"""Supprime le fichier licence. Retourne True s'il existait."""
|
|
try:
|
|
self._path.unlink()
|
|
return True
|
|
except FileNotFoundError:
|
|
return False
|