attachment backend use
This commit is contained in:
@@ -4,7 +4,7 @@ from pathlib import PurePosixPath
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.models import CampaignAttachmentUse, CampaignJob, FileAsset
|
||||
from app.db.models import CampaignAttachmentUse, CampaignJob, FileAsset, FileBlob, FileVersion
|
||||
from app.storage.common import utcnow
|
||||
from app.storage.files import current_version_and_blob, list_assets_for_user
|
||||
|
||||
@@ -17,17 +17,60 @@ def _candidate_match_keys(raw_match: str) -> set[str]:
|
||||
return {item for item in result if item}
|
||||
|
||||
|
||||
def record_campaign_attachment_uses_for_job(session: Session, job: CampaignJob, *, stage: str = "built") -> None:
|
||||
"""Create best-effort immutable file-use records for matched managed files.
|
||||
def _add_use(
|
||||
session: Session,
|
||||
job: CampaignJob,
|
||||
*,
|
||||
asset: FileAsset,
|
||||
version: FileVersion,
|
||||
blob: FileBlob,
|
||||
filename_used: str,
|
||||
stage: str,
|
||||
) -> None:
|
||||
exists = (
|
||||
session.query(CampaignAttachmentUse)
|
||||
.filter(
|
||||
CampaignAttachmentUse.campaign_job_id == job.id,
|
||||
CampaignAttachmentUse.file_version_id == version.id,
|
||||
CampaignAttachmentUse.filename_used == filename_used,
|
||||
CampaignAttachmentUse.use_stage == stage,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if exists:
|
||||
return
|
||||
session.add(
|
||||
CampaignAttachmentUse(
|
||||
tenant_id=job.tenant_id,
|
||||
campaign_id=job.campaign_id,
|
||||
campaign_version_id=job.campaign_version_id,
|
||||
campaign_job_id=job.id,
|
||||
entry_index=job.entry_index,
|
||||
entry_id=job.entry_id,
|
||||
file_asset_id=asset.id,
|
||||
file_version_id=version.id,
|
||||
file_blob_id=blob.id,
|
||||
filename_used=filename_used,
|
||||
checksum_sha256=blob.checksum_sha256,
|
||||
size_bytes=blob.size_bytes,
|
||||
content_type=blob.content_type,
|
||||
use_stage=stage,
|
||||
)
|
||||
)
|
||||
|
||||
Existing attachment resolution is still filesystem/path based. This bridge
|
||||
records uses when a resolved attachment match can be tied to a managed file
|
||||
by logical path or filename among files shared with the campaign.
|
||||
|
||||
def record_campaign_attachment_uses_for_job(session: Session, job: CampaignJob, *, stage: str = "built") -> None:
|
||||
"""Record immutable managed file versions used by a built/sent job.
|
||||
|
||||
New builds carry exact managed asset/version/blob IDs. Filename matching is
|
||||
retained only as a compatibility fallback for jobs created before managed
|
||||
attachment materialization was introduced.
|
||||
"""
|
||||
|
||||
attachments = job.resolved_attachments or []
|
||||
if not isinstance(attachments, list):
|
||||
return
|
||||
|
||||
assets = list_assets_for_user(
|
||||
session,
|
||||
tenant_id=job.tenant_id,
|
||||
@@ -35,12 +78,40 @@ def record_campaign_attachment_uses_for_job(session: Session, job: CampaignJob,
|
||||
campaign_id=job.campaign_id,
|
||||
is_admin=True,
|
||||
)
|
||||
assets_by_id = {asset.id: asset for asset in assets}
|
||||
|
||||
for attachment in attachments:
|
||||
if not isinstance(attachment, dict):
|
||||
continue
|
||||
managed_matches = attachment.get("managed_matches")
|
||||
if isinstance(managed_matches, list):
|
||||
for item in managed_matches:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
asset = assets_by_id.get(str(item.get("asset_id") or ""))
|
||||
version = session.get(FileVersion, str(item.get("version_id") or ""))
|
||||
blob = session.get(FileBlob, str(item.get("blob_id") or ""))
|
||||
if not asset or not version or not blob:
|
||||
continue
|
||||
if version.file_asset_id != asset.id or version.blob_id != blob.id:
|
||||
continue
|
||||
_add_use(
|
||||
session,
|
||||
job,
|
||||
asset=asset,
|
||||
version=version,
|
||||
blob=blob,
|
||||
filename_used=str(item.get("filename") or asset.filename),
|
||||
stage=stage,
|
||||
)
|
||||
|
||||
# Compatibility fallback for older job snapshots without managed_matches.
|
||||
by_key: dict[str, FileAsset] = {}
|
||||
for asset in assets:
|
||||
by_key[asset.display_path.strip("/")] = asset
|
||||
by_key[asset.filename] = asset
|
||||
by_key.setdefault(asset.filename, asset)
|
||||
for attachment in attachments:
|
||||
if not isinstance(attachment, dict):
|
||||
if not isinstance(attachment, dict) or attachment.get("managed_matches"):
|
||||
continue
|
||||
matches = attachment.get("matches") if isinstance(attachment.get("matches"), list) else []
|
||||
for raw in matches:
|
||||
@@ -50,35 +121,14 @@ def record_campaign_attachment_uses_for_job(session: Session, job: CampaignJob,
|
||||
if not asset:
|
||||
continue
|
||||
version, blob = current_version_and_blob(session, asset)
|
||||
exists = (
|
||||
session.query(CampaignAttachmentUse)
|
||||
.filter(
|
||||
CampaignAttachmentUse.campaign_job_id == job.id,
|
||||
CampaignAttachmentUse.file_version_id == version.id,
|
||||
CampaignAttachmentUse.filename_used == asset.filename,
|
||||
CampaignAttachmentUse.use_stage == stage,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
if exists:
|
||||
continue
|
||||
session.add(
|
||||
CampaignAttachmentUse(
|
||||
tenant_id=job.tenant_id,
|
||||
campaign_id=job.campaign_id,
|
||||
campaign_version_id=job.campaign_version_id,
|
||||
campaign_job_id=job.id,
|
||||
entry_index=job.entry_index,
|
||||
entry_id=job.entry_id,
|
||||
file_asset_id=asset.id,
|
||||
file_version_id=version.id,
|
||||
file_blob_id=blob.id,
|
||||
filename_used=asset.filename,
|
||||
checksum_sha256=blob.checksum_sha256,
|
||||
size_bytes=blob.size_bytes,
|
||||
content_type=blob.content_type,
|
||||
use_stage=stage,
|
||||
)
|
||||
_add_use(
|
||||
session,
|
||||
job,
|
||||
asset=asset,
|
||||
version=version,
|
||||
blob=blob,
|
||||
filename_used=asset.filename,
|
||||
stage=stage,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user