fix(export_glb): upsert MediaAsset instead of DELETE+INSERT to preserve stable URLs

When re-generating a production or geometry GLB, the old approach deleted the
existing MediaAsset record and created a new one with a new UUID. Any page that
had the old download_url (/api/media/{old-id}/download) cached would then get
a 404 when trying to download, because the asset ID no longer existed in the DB.

Fix: update the existing MediaAsset record in-place (same UUID, new storage_key)
so existing download URLs remain valid after regeneration. Create a new record
only if no existing one is found.

Applies to both generate_gltf_geometry_task and generate_gltf_production_task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 10:56:25 +01:00
parent b3c4be45f6
commit d1c7feacf6
@@ -124,36 +124,48 @@ def generate_gltf_geometry_task(self, cad_file_id: str):
log_task_event(self.request.id, f"OCC GLB export completed: {output_path.name}", "done")
# --- Store MediaAsset (replace existing gltf_geometry for this cad_file) ---
# --- Store MediaAsset (upsert: update existing to keep stable ID/URL) ---
import uuid as _uuid
from sqlalchemy import create_engine as _ce, delete as _del
from sqlalchemy import create_engine as _ce, select as _sel2
from sqlalchemy.orm import Session as _Session
from app.domains.media.models import MediaAsset, MediaAssetType
_sync_url = app_settings.database_url.replace("+asyncpg", "")
_eng2 = _ce(_sync_url)
with _Session(_eng2) as _sess:
_sess.execute(
_del(MediaAsset).where(
MediaAsset.cad_file_id == _uuid.UUID(cad_file_id),
MediaAsset.asset_type == MediaAssetType.gltf_geometry,
)
)
_key = str(output_path)
_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
if _key.startswith(_prefix):
_key = _key[len(_prefix):]
asset = MediaAsset(
cad_file_id=_uuid.UUID(cad_file_id),
product_id=_uuid.UUID(product_id) if product_id else None,
asset_type=MediaAssetType.gltf_geometry,
storage_key=_key,
mime_type="model/gltf-binary",
file_size_bytes=output_path.stat().st_size if output_path.exists() else None,
)
_sess.add(asset)
_sess.commit()
asset_id = str(asset.id)
_file_size = output_path.stat().st_size if output_path.exists() else None
existing = _sess.execute(
_sel2(MediaAsset).where(
MediaAsset.cad_file_id == _uuid.UUID(cad_file_id),
MediaAsset.asset_type == MediaAssetType.gltf_geometry,
)
).scalars().first()
if existing:
existing.storage_key = _key
existing.mime_type = "model/gltf-binary"
existing.file_size_bytes = _file_size
if product_id:
existing.product_id = _uuid.UUID(product_id)
_sess.commit()
asset_id = str(existing.id)
else:
asset = MediaAsset(
cad_file_id=_uuid.UUID(cad_file_id),
product_id=_uuid.UUID(product_id) if product_id else None,
asset_type=MediaAssetType.gltf_geometry,
storage_key=_key,
mime_type="model/gltf-binary",
file_size_bytes=_file_size,
)
_sess.add(asset)
_sess.commit()
asset_id = str(asset.id)
_eng2.dispose()
pl.step_done("export_glb_geometry", result={"glb_path": str(output_path), "asset_id": asset_id})
@@ -182,7 +194,7 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
import uuid as _uuid
from pathlib import Path as _Path
from sqlalchemy import create_engine as _ce, delete as _del, select as _sel
from sqlalchemy import create_engine as _ce, delete as _del, select as _sel, update as _upd
from sqlalchemy.orm import Session as _Session
from app.config import settings as app_settings
@@ -205,6 +217,7 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
_sel(_CF).where(_CF.id == _uuid.UUID(cad_file_id))
).scalar_one_or_none()
step_path_str = _cad.stored_path if _cad else None
cad_mesh_attributes: dict = (_cad.mesh_attributes or {}) if _cad else {}
settings_rows = _sess.execute(_sel(SystemSetting)).scalars().all()
sys_settings = {s.key: s.value for s in settings_rows}
@@ -302,6 +315,7 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
"--output_path", str(output_path),
"--material_map", _json.dumps(mat_map),
"--smooth_angle", str(smooth_angle),
"--mesh_attributes", _json.dumps(cad_mesh_attributes),
]
if asset_library_blend:
cmd += ["--asset_library_blend", asset_library_blend]
@@ -333,30 +347,44 @@ def generate_gltf_production_task(self, cad_file_id: str, product_id: str | None
log_task_event(self.request.id, f"Production GLB exported: {output_path.name}", "done")
# --- 4. Store MediaAsset (replace existing gltf_production for this cad_file) ---
# --- 4. Store MediaAsset (upsert: update existing record to keep stable ID/URL) ---
# Updating in-place (not DELETE+INSERT) preserves the existing asset UUID so that
# any frontend page holding a stale download_url continues to resolve correctly.
_eng2 = _ce(_sync_url)
with _Session(_eng2) as _sess:
_sess.execute(
_del(MediaAsset).where(
MediaAsset.cad_file_id == _uuid.UUID(cad_file_id),
MediaAsset.asset_type == MediaAssetType.gltf_production,
)
)
_key = str(output_path)
_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
if _key.startswith(_prefix):
_key = _key[len(_prefix):]
asset = MediaAsset(
cad_file_id=_uuid.UUID(cad_file_id),
product_id=_uuid.UUID(product_id) if product_id else None,
asset_type=MediaAssetType.gltf_production,
storage_key=_key,
mime_type="model/gltf-binary",
file_size_bytes=output_path.stat().st_size if output_path.exists() else None,
)
_sess.add(asset)
_sess.commit()
asset_id = str(asset.id)
_file_size = output_path.stat().st_size if output_path.exists() else None
existing = _sess.execute(
_sel(MediaAsset).where(
MediaAsset.cad_file_id == _uuid.UUID(cad_file_id),
MediaAsset.asset_type == MediaAssetType.gltf_production,
)
).scalars().first()
if existing:
existing.storage_key = _key
existing.mime_type = "model/gltf-binary"
existing.file_size_bytes = _file_size
if product_id:
existing.product_id = _uuid.UUID(product_id)
_sess.commit()
asset_id = str(existing.id)
else:
asset = MediaAsset(
cad_file_id=_uuid.UUID(cad_file_id),
product_id=_uuid.UUID(product_id) if product_id else None,
asset_type=MediaAssetType.gltf_production,
storage_key=_key,
mime_type="model/gltf-binary",
file_size_bytes=_file_size,
)
_sess.add(asset)
_sess.commit()
asset_id = str(asset.id)
_eng2.dispose()
pl.step_done("export_glb_production", result={"glb_path": str(output_path), "asset_id": asset_id})