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:
@@ -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")
|
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
|
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 sqlalchemy.orm import Session as _Session
|
||||||
from app.domains.media.models import MediaAsset, MediaAssetType
|
from app.domains.media.models import MediaAsset, MediaAssetType
|
||||||
|
|
||||||
_sync_url = app_settings.database_url.replace("+asyncpg", "")
|
_sync_url = app_settings.database_url.replace("+asyncpg", "")
|
||||||
_eng2 = _ce(_sync_url)
|
_eng2 = _ce(_sync_url)
|
||||||
with _Session(_eng2) as _sess:
|
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)
|
_key = str(output_path)
|
||||||
_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
|
_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
|
||||||
if _key.startswith(_prefix):
|
if _key.startswith(_prefix):
|
||||||
_key = _key[len(_prefix):]
|
_key = _key[len(_prefix):]
|
||||||
asset = MediaAsset(
|
_file_size = output_path.stat().st_size if output_path.exists() else None
|
||||||
cad_file_id=_uuid.UUID(cad_file_id),
|
|
||||||
product_id=_uuid.UUID(product_id) if product_id else None,
|
existing = _sess.execute(
|
||||||
asset_type=MediaAssetType.gltf_geometry,
|
_sel2(MediaAsset).where(
|
||||||
storage_key=_key,
|
MediaAsset.cad_file_id == _uuid.UUID(cad_file_id),
|
||||||
mime_type="model/gltf-binary",
|
MediaAsset.asset_type == MediaAssetType.gltf_geometry,
|
||||||
file_size_bytes=output_path.stat().st_size if output_path.exists() else None,
|
)
|
||||||
)
|
).scalars().first()
|
||||||
_sess.add(asset)
|
|
||||||
_sess.commit()
|
if existing:
|
||||||
asset_id = str(asset.id)
|
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()
|
_eng2.dispose()
|
||||||
|
|
||||||
pl.step_done("export_glb_geometry", result={"glb_path": str(output_path), "asset_id": asset_id})
|
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
|
import uuid as _uuid
|
||||||
from pathlib import Path as _Path
|
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 sqlalchemy.orm import Session as _Session
|
||||||
|
|
||||||
from app.config import settings as app_settings
|
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))
|
_sel(_CF).where(_CF.id == _uuid.UUID(cad_file_id))
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
step_path_str = _cad.stored_path if _cad else 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()
|
settings_rows = _sess.execute(_sel(SystemSetting)).scalars().all()
|
||||||
sys_settings = {s.key: s.value for s in settings_rows}
|
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),
|
"--output_path", str(output_path),
|
||||||
"--material_map", _json.dumps(mat_map),
|
"--material_map", _json.dumps(mat_map),
|
||||||
"--smooth_angle", str(smooth_angle),
|
"--smooth_angle", str(smooth_angle),
|
||||||
|
"--mesh_attributes", _json.dumps(cad_mesh_attributes),
|
||||||
]
|
]
|
||||||
if asset_library_blend:
|
if asset_library_blend:
|
||||||
cmd += ["--asset_library_blend", 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")
|
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)
|
_eng2 = _ce(_sync_url)
|
||||||
with _Session(_eng2) as _sess:
|
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)
|
_key = str(output_path)
|
||||||
_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
|
_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
|
||||||
if _key.startswith(_prefix):
|
if _key.startswith(_prefix):
|
||||||
_key = _key[len(_prefix):]
|
_key = _key[len(_prefix):]
|
||||||
asset = MediaAsset(
|
_file_size = output_path.stat().st_size if output_path.exists() else None
|
||||||
cad_file_id=_uuid.UUID(cad_file_id),
|
|
||||||
product_id=_uuid.UUID(product_id) if product_id else None,
|
existing = _sess.execute(
|
||||||
asset_type=MediaAssetType.gltf_production,
|
_sel(MediaAsset).where(
|
||||||
storage_key=_key,
|
MediaAsset.cad_file_id == _uuid.UUID(cad_file_id),
|
||||||
mime_type="model/gltf-binary",
|
MediaAsset.asset_type == MediaAssetType.gltf_production,
|
||||||
file_size_bytes=output_path.stat().st_size if output_path.exists() else None,
|
)
|
||||||
)
|
).scalars().first()
|
||||||
_sess.add(asset)
|
|
||||||
_sess.commit()
|
if existing:
|
||||||
asset_id = str(asset.id)
|
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()
|
_eng2.dispose()
|
||||||
|
|
||||||
pl.step_done("export_glb_production", result={"glb_path": str(output_path), "asset_id": asset_id})
|
pl.step_done("export_glb_production", result={"glb_path": str(output_path), "asset_id": asset_id})
|
||||||
|
|||||||
Reference in New Issue
Block a user