Structure de dossiers par utilisateur : projects/<propriétaire>/<projet>/
Build and deploy / test-python (push) Failing after 3m14s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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>
This commit is contained in:
william
2026-09-02 06:22:03 +02:00
co-authored by Claude Sonnet 5
parent d5a59bdbd8
commit 0f23024882
10 changed files with 245 additions and 55 deletions
+84
View File
@@ -0,0 +1,84 @@
"""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)