ops: un deploiement qui refuse de partir casse, et qui se verifie
OPS-011, en partie. Ce que le workflow garantit maintenant : - rien ne part d un arbre casse. La suite, ruff check et ruff format tournent sur le runner de deploiement avant tout envoi. Une CI verte sur GitHub ne prouve rien ici : le deploiement se declenche a la main, sur ce que la branche contient a cet instant ; - seuls les fichiers nommes partent. La charge est une liste blanche — app/, wsgi.py, requirements.txt — et non l arbre de travail moins neuf exclusions. C est par cette porte que clear_db.py, la suite de tests et les definitions de CI se sont retrouves sur le noeud de production ; - le deploiement est verifie. /health est interroge pendant deux minutes apres l envoi et le job echoue s il ne repond jamais « healthy ». Avant, un arbre a moitie televerse etait un deploiement vert. Ce qui n est pas garanti, et c est ecrit dans le fichier : la bascule n est pas atomique. Le miroir se fait sur place, donc pendant le transfert la production execute un melange de deux versions. En cherchant a fermer ce point, un defaut a part entiere est apparu. Les contrats etaient ranges a os.getcwd()/documents et leur chemin absolu ecrit en base. La racine de stockage suivait donc le repertoire depuis lequel le processus avait ete lance : redemarrer le serveur ailleurs envoie les nouveaux contrats dans un nouvel arbre et rend les anciens illisibles — la base continuant d affirmer qu ils sont la, la panne se manifeste par un 500 au telechargement, pas par quelque chose d actionnable. app/storage.py fixe la racine et DOCUMENTS_ROOT la deplace. Les nouvelles lignes gardent un chemin relatif, les anciennes gardent leur chemin absolu et continuent de resoudre : aucune migration de donnees n est necessaire, donc ce changement n attend pas Alembic. C etait aussi le troisieme pre-requis de la bascule par repertoires de version. Les deux autres sont hors d atteinte d ici — la commande de demarrage Pterodactyl doit pointer sur current/, et les repertoires partages doivent etre installes sur le noeud. Les deux sont decrits dans docs/deployment.md, avec la procedure de retour arriere qui manquait. 511 tests.
This commit is contained in:
@@ -19,6 +19,7 @@ from app.routes.users._shared import (
|
||||
pdf_upload_error,
|
||||
)
|
||||
from app.routes.users.blueprint import users_bp
|
||||
from app.storage import CONTRACTS_DIR, document_path
|
||||
from app.validators import UploadContractSchema
|
||||
|
||||
|
||||
@@ -108,25 +109,24 @@ def upload_contract():
|
||||
flash(error, 'danger')
|
||||
return redirect(url_for('users.upload_contract'))
|
||||
|
||||
upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés')
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
player = User.query.get_or_404(player_id)
|
||||
player_teams = player.get_org_teams()
|
||||
team = player_teams[0] if player_teams else None
|
||||
|
||||
if team:
|
||||
team_folder = os.path.join(upload_dir, secure_filename(team.name))
|
||||
os.makedirs(team_folder, exist_ok=True)
|
||||
final_dir = team_folder
|
||||
else:
|
||||
final_dir = upload_dir
|
||||
|
||||
original_filename = secure_filename(file.filename)
|
||||
file_uuid = str(uuid.uuid4())
|
||||
stored_filename = f"{file_uuid}.pdf"
|
||||
file_path = os.path.join(final_dir, stored_filename)
|
||||
file.save(file_path)
|
||||
stored_filename = f"{uuid.uuid4()}.pdf"
|
||||
|
||||
# Kept relative to the document root, not absolute (see app/storage.py):
|
||||
# an absolute path pins the file to the directory the process was
|
||||
# started from, which is the one thing a release-directory deploy
|
||||
# changes.
|
||||
relative_path = os.path.join(CONTRACTS_DIR, stored_filename)
|
||||
if team:
|
||||
relative_path = os.path.join(CONTRACTS_DIR, secure_filename(team.name), stored_filename)
|
||||
|
||||
absolute_path = document_path(relative_path)
|
||||
os.makedirs(os.path.dirname(absolute_path), exist_ok=True)
|
||||
file.save(absolute_path)
|
||||
|
||||
contract = Contract(
|
||||
player_id=player_id,
|
||||
@@ -134,7 +134,7 @@ def upload_contract():
|
||||
uploaded_by_id=current_user.id,
|
||||
original_filename=original_filename,
|
||||
stored_filename=stored_filename,
|
||||
file_path=file_path,
|
||||
file_path=relative_path,
|
||||
notes=notes if notes else None,
|
||||
)
|
||||
db.session.add(contract)
|
||||
@@ -164,12 +164,11 @@ def upload_signed_contract(contract_id):
|
||||
return redirect(url_for('users.list_contracts'))
|
||||
|
||||
signed_filename = f"signed_{contract.stored_filename}"
|
||||
file.save(contract.file_path.replace(contract.stored_filename, signed_filename))
|
||||
signed_path = contract.file_path.replace(contract.stored_filename, signed_filename)
|
||||
file.save(document_path(signed_path))
|
||||
|
||||
contract.signed_filename = signed_filename
|
||||
contract.signed_file_path = contract.file_path.replace(
|
||||
contract.stored_filename, signed_filename
|
||||
)
|
||||
contract.signed_file_path = signed_path
|
||||
contract.status = 'signed'
|
||||
contract.signed_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
@@ -186,7 +185,9 @@ def download_contract(contract_id):
|
||||
flash(_('You do not have permission to download this contract.'), 'danger')
|
||||
return redirect(url_for('users.list_contracts'))
|
||||
return send_file(
|
||||
contract.file_path, as_attachment=True, download_name=contract.original_filename
|
||||
document_path(contract.file_path),
|
||||
as_attachment=True,
|
||||
download_name=contract.original_filename,
|
||||
)
|
||||
|
||||
|
||||
@@ -202,5 +203,7 @@ def download_signed_contract(contract_id):
|
||||
flash(_('No signed contract available.'), 'danger')
|
||||
return redirect(url_for('users.list_contracts'))
|
||||
return send_file(
|
||||
contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename
|
||||
document_path(contract.signed_file_path),
|
||||
as_attachment=True,
|
||||
download_name=contract.signed_filename,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Where uploaded documents live, and how the database refers to them.
|
||||
|
||||
Contracts were stored at `os.path.join(os.getcwd(), 'documents', …)`,
|
||||
evaluated at upload time, and the resulting absolute path was written into
|
||||
`Contract.file_path`. The storage root therefore moved with whatever
|
||||
directory the process happened to be started from. Two consequences:
|
||||
|
||||
- one latent: start the server from elsewhere and new contracts land in a
|
||||
new tree while the old ones become unreadable — with the database still
|
||||
saying they are there, so the failure surfaces as a 500 on download
|
||||
rather than as anything a person could act on;
|
||||
- one blocking: it rules out a release-directory deployment (OPS-011)
|
||||
outright. Every stored path would point inside a release that is about
|
||||
to be replaced, so the first switch would take every contract ever
|
||||
uploaded with it.
|
||||
|
||||
New rows keep a path *relative* to the document root. Old rows keep their
|
||||
absolute path and are returned untouched, so this change needs no data
|
||||
migration and can ship before Alembic does (DB-002).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
#: Environment override for the document root. What a release-directory
|
||||
#: deployment sets, to a path outside the releases — alongside them, not
|
||||
#: inside whichever one is current.
|
||||
DOCUMENTS_ROOT_ENV = 'DOCUMENTS_ROOT'
|
||||
|
||||
#: Sub-directory holding uploaded contracts, under the document root.
|
||||
CONTRACTS_DIR = 'contrats signés'
|
||||
|
||||
|
||||
def documents_root():
|
||||
"""Absolute path of the document store.
|
||||
|
||||
Falls back to `documents/` beside this package — the project root
|
||||
wherever it is installed, rather than wherever the process was launched.
|
||||
"""
|
||||
configured = os.getenv(DOCUMENTS_ROOT_ENV)
|
||||
if configured:
|
||||
return os.path.abspath(configured)
|
||||
package_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.join(os.path.dirname(package_dir), 'documents')
|
||||
|
||||
|
||||
def document_path(stored_path):
|
||||
"""Absolute path of a document, from what the database holds.
|
||||
|
||||
Args:
|
||||
stored_path: The value of Contract.file_path or signed_file_path.
|
||||
Relative for rows written since this module existed, absolute
|
||||
for the ones written before.
|
||||
|
||||
Returns:
|
||||
str: An absolute path.
|
||||
"""
|
||||
if os.path.isabs(stored_path):
|
||||
return stored_path
|
||||
return os.path.join(documents_root(), stored_path)
|
||||
Reference in New Issue
Block a user