feat(files): upload/preview/delete allegati su fatture, ULA, documenti
- models: colonne file inline (storage_path, mime, size_bytes, sha256, uploaded_by, uploaded_at) su remission_invoice, remission_ula_employee, remission_document - migrations: ALTER idempotente al lifespan per evolvere schema in sandbox - storage: FS adapter /var/uploads con validazione MIME/size, dedup sha256, sanitize - routers/files: POST upload / GET download (con ?inline=1) / DELETE matrix autorizzazioni: beneficiary su DRAFT|AWAITING_AMENDMENT, istruttore read-only, superadmin full - main: include router files, version bump 0.2.0 Testato E2E con admin JWT: upload 549B PDF -> DB coerente, storage 1/invoice/<uuid>/<sha12>-file.pdf, download con magic bytes PDF corretti, delete chirurgico con cleanup FS e metadata.
This commit is contained in:
56
app/migrations.py
Normal file
56
app/migrations.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Migration idempotente per sandbox.
|
||||
Base.metadata.create_all() crea solo tabelle mancanti, non colonne aggiuntive.
|
||||
Qui gestiamo ALTER TABLE ADD COLUMN IF NOT EXISTS per l'evoluzione dello schema.
|
||||
|
||||
Ogni migration è una stringa SQL che puo essere eseguita piu volte senza errori.
|
||||
"""
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("rendicontazione-api.migrations")
|
||||
|
||||
|
||||
MIGRATIONS = [
|
||||
# 2026-04-18: colonne file upload su remission_invoice
|
||||
"""
|
||||
ALTER TABLE gepafin_rendic.remission_invoice
|
||||
ADD COLUMN IF NOT EXISTS storage_path varchar(1024),
|
||||
ADD COLUMN IF NOT EXISTS mime varchar(128),
|
||||
ADD COLUMN IF NOT EXISTS size_bytes bigint,
|
||||
ADD COLUMN IF NOT EXISTS sha256 varchar(64),
|
||||
ADD COLUMN IF NOT EXISTS uploaded_by integer,
|
||||
ADD COLUMN IF NOT EXISTS uploaded_at timestamptz;
|
||||
""",
|
||||
# 2026-04-18: colonne file upload su remission_ula_employee
|
||||
"""
|
||||
ALTER TABLE gepafin_rendic.remission_ula_employee
|
||||
ADD COLUMN IF NOT EXISTS storage_path varchar(1024),
|
||||
ADD COLUMN IF NOT EXISTS mime varchar(128),
|
||||
ADD COLUMN IF NOT EXISTS size_bytes bigint,
|
||||
ADD COLUMN IF NOT EXISTS sha256 varchar(64),
|
||||
ADD COLUMN IF NOT EXISTS uploaded_by integer,
|
||||
ADD COLUMN IF NOT EXISTS uploaded_at timestamptz;
|
||||
""",
|
||||
# 2026-04-18: colonne file upload su remission_document
|
||||
"""
|
||||
ALTER TABLE gepafin_rendic.remission_document
|
||||
ADD COLUMN IF NOT EXISTS storage_path varchar(1024),
|
||||
ADD COLUMN IF NOT EXISTS mime varchar(128),
|
||||
ADD COLUMN IF NOT EXISTS size_bytes bigint,
|
||||
ADD COLUMN IF NOT EXISTS sha256 varchar(64),
|
||||
ADD COLUMN IF NOT EXISTS uploaded_by integer;
|
||||
""",
|
||||
]
|
||||
|
||||
|
||||
def run_migrations(engine) -> None:
|
||||
"""Esegue tutte le migration in transazione. Log su ciascuna."""
|
||||
with engine.begin() as conn:
|
||||
for i, sql in enumerate(MIGRATIONS, 1):
|
||||
try:
|
||||
conn.execute(text(sql))
|
||||
log.info(f"migration {i}/{len(MIGRATIONS)} OK")
|
||||
except Exception as e:
|
||||
log.error(f"migration {i} FAILED: {e}")
|
||||
raise
|
||||
Reference in New Issue
Block a user