Structure de dossiers par utilisateur : projects/<propriétaire>/<projet>/
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:
co-authored by
Claude Sonnet 5
parent
d5a59bdbd8
commit
0f23024882
@@ -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)
|
||||
Reference in New Issue
Block a user