Remplace le rangement plat projects/<slug>/ par une vraie arborescence par compte créateur, sans toucher aux routes existantes (aucune ne déclare de "/" dans son <slug> : le chemin composé "propriétaire_projet" reste un détail interne à db/, décodé par db/games/project_slug.py, seul fichier à modifier si la convention change un jour). - db/game_dir.py, create_game.py, delete_game.py, list_games.py, move_game.py : résolvent/construisent ce chemin composé. Bénéfice direct : deux comptes différents peuvent chacun avoir un projet nommé pareil sans collision (l'unicité ne se vérifie plus que par dossier propriétaire). move_game.py renomme désormais le dossier PROPRIÉTAIRE entier (changement d'email), prêt pour un futur multi-projet. - scripts/migrate_flat_project_slugs.py : migration des projets déjà créés sous l'ancien rangement plat (simulation par défaut, --apply pour exécuter). - routes/auth/profile.py, tests/conftest.py : suppression de compte/jeu de test via db.delete_game (nettoie aussi le dossier propriétaire devenu vide) plutôt qu'un shutil.rmtree direct du seul dossier projet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
85 lines
3.5 KiB
Python
85 lines
3.5 KiB
Python
"""Script à usage unique (PAS exécuté au runtime du moteur) : migre les
|
|
projets encore rangés à plat (projects/<slug>/game.db, l'ancien schéma)
|
|
vers la structure par utilisateur (projects/<owner_folder>/<project_part>/,
|
|
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)
|