"""Script à usage unique (PAS exécuté au runtime du moteur) : migre les projets encore rangés à plat (projects//game.db, l'ancien schéma) vers la structure par utilisateur (projects///, voir db/games/project_slug.py et le plan "Structure de dossiers propriétaire/projet") — db/game_dir.py sait déjà lire les deux formes, cette migration n'est donc pas bloquante pour faire tourner le moteur, mais range enfin les dossiers comme voulu et met à jour project_slug (voir auth/set_project_slug.py) pour les comptes concernés. owner_folder = l'ancien slug plat lui-même : c'était déjà, historiquement, slugify(email) pour le SEUL projet d'un compte "user" (voir l'ancienne version de routes/auth/register_2fa.py, avant l'onboarding guidé) — donc le bon dossier propriétaire, tel quel. project_part = slugify(nom du jeu stocké en _meta), pour donner un vrai nom de dossier PROJET distinct. Mode simulation par défaut (n'écrit rien, affiche ce qui serait fait) — `--apply` requis pour exécuter réellement : python scripts/migrate_flat_project_slugs.py # aperçu python scripts/migrate_flat_project_slugs.py --apply # exécute """ import os import sys _BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, _BASE_DIR) import db # noqa: E402 import auth # noqa: E402 from db.constants import PROJECTS_DIR # noqa: E402 from db.games.project_slug import build_slug # noqa: E402 from auth.connection import connect as connect_auth_db # noqa: E402 def _flat_project_slugs(): """Slugs plats trouvés directement sous projects/ (un dossier avec game.db à la racine, pas dans un sous-dossier propriétaire).""" if not os.path.isdir(PROJECTS_DIR): return [] found = [] for entry in sorted(os.listdir(PROJECTS_DIR)): entry_path = os.path.join(PROJECTS_DIR, entry) if os.path.isfile(os.path.join(entry_path, "game.db")): found.append(entry) return found def migrate(apply=False): slugs = _flat_project_slugs() if not slugs: print("Aucun projet à plat trouvé — rien à migrer.") return conn = connect_auth_db() for old_slug in slugs: game_meta = db.game_meta(old_slug) project_part = db.slugify(game_meta["name"]) or "jeu" new_slug = build_slug(old_slug, project_part) owning_users = [ dict(r) for r in conn.execute("SELECT id, email FROM _users WHERE project_slug = ?", (old_slug,)).fetchall() ] print(f"{old_slug!r} -> {new_slug!r}" f" (comptes concernés : {[u['email'] for u in owning_users] or 'aucun'})") if not apply: continue old_dir = os.path.join(PROJECTS_DIR, old_slug) new_dir = os.path.join(PROJECTS_DIR, old_slug, project_part) # Renommer EN PLACE (dossier propriétaire = ancien dossier projet, # déplacé dans son propre sous-dossier) demanderait un dossier # intermédiaire — plus sûr de passer par un nom temporaire. tmp_dir = old_dir + ".migrating" os.rename(old_dir, tmp_dir) os.makedirs(os.path.dirname(new_dir), exist_ok=True) os.rename(tmp_dir, new_dir) for user in owning_users: auth.set_project_slug(user["id"], new_slug) conn.close() if apply: print(f"{len(slugs)} projet(s) migré(s).") else: print("Aperçu seulement — relance avec --apply pour exécuter.") if __name__ == "__main__": migrate(apply="--apply" in sys.argv)