128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import PurePosixPath
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.db.models import CampaignAttachmentUse, CampaignJob, FileAsset
|
|
from app.storage.common import utcnow
|
|
from app.storage.files import current_version_and_blob, list_assets_for_user
|
|
|
|
|
|
def _candidate_match_keys(raw_match: str) -> set[str]:
|
|
cleaned = raw_match.replace("\\", "/").strip().strip("/")
|
|
result = {cleaned}
|
|
if cleaned:
|
|
result.add(PurePosixPath(cleaned).name)
|
|
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.
|
|
|
|
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.
|
|
"""
|
|
|
|
attachments = job.resolved_attachments or []
|
|
if not isinstance(attachments, list):
|
|
return
|
|
assets = list_assets_for_user(
|
|
session,
|
|
tenant_id=job.tenant_id,
|
|
user_id="",
|
|
campaign_id=job.campaign_id,
|
|
is_admin=True,
|
|
)
|
|
by_key: dict[str, FileAsset] = {}
|
|
for asset in assets:
|
|
by_key[asset.display_path.strip("/")] = asset
|
|
by_key[asset.filename] = asset
|
|
for attachment in attachments:
|
|
if not isinstance(attachment, dict):
|
|
continue
|
|
matches = attachment.get("matches") if isinstance(attachment.get("matches"), list) else []
|
|
for raw in matches:
|
|
if not isinstance(raw, str):
|
|
continue
|
|
asset = next((by_key[key] for key in _candidate_match_keys(raw) if key in by_key), None)
|
|
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,
|
|
)
|
|
)
|
|
|
|
|
|
def mark_job_attachment_uses_sent(session: Session, job: CampaignJob) -> None:
|
|
record_campaign_attachment_uses_for_job(session, job, stage="built")
|
|
now = utcnow()
|
|
uses = (
|
|
session.query(CampaignAttachmentUse)
|
|
.filter(
|
|
CampaignAttachmentUse.tenant_id == job.tenant_id,
|
|
CampaignAttachmentUse.campaign_job_id == job.id,
|
|
CampaignAttachmentUse.use_stage == "built",
|
|
)
|
|
.all()
|
|
)
|
|
for use in uses:
|
|
sent = (
|
|
session.query(CampaignAttachmentUse)
|
|
.filter(
|
|
CampaignAttachmentUse.campaign_job_id == job.id,
|
|
CampaignAttachmentUse.file_version_id == use.file_version_id,
|
|
CampaignAttachmentUse.use_stage == "sent",
|
|
)
|
|
.one_or_none()
|
|
)
|
|
if sent:
|
|
continue
|
|
session.add(
|
|
CampaignAttachmentUse(
|
|
tenant_id=use.tenant_id,
|
|
campaign_id=use.campaign_id,
|
|
campaign_version_id=use.campaign_version_id,
|
|
campaign_job_id=use.campaign_job_id,
|
|
entry_index=use.entry_index,
|
|
entry_id=use.entry_id,
|
|
file_asset_id=use.file_asset_id,
|
|
file_version_id=use.file_version_id,
|
|
file_blob_id=use.file_blob_id,
|
|
filename_used=use.filename_used,
|
|
checksum_sha256=use.checksum_sha256,
|
|
size_bytes=use.size_bytes,
|
|
content_type=use.content_type,
|
|
use_stage="sent",
|
|
used_at=now,
|
|
)
|
|
)
|