Skip to content

feat(attachments): admin backfill re-detects legacy octet-stream crystals - #321

Open
lopugit wants to merge 1 commit into
developfrom
claude/attachment-detected-type-backfill
Open

feat(attachments): admin backfill re-detects legacy octet-stream crystals#321
lopugit wants to merge 1 commit into
developfrom
claude/attachment-detected-type-backfill

Conversation

@lopugit

@lopugit lopugit commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Summary

PR #319 added magic-byte detection at upload completion, but only for uploads finalized after it deployed. Ready attachments finalized earlier keep crystal.contentType: application/octet-stream with no detectedContentType — e.g. the owner's QuickTime-container screen recording posted 2026-08-19 still renders as a file card. This PR adds an admin-only, batched, idempotent backfill that re-runs detection for those legacy rows and publishes exactly what completion would have.

What it does

POST /api/v1/attachments/backfill-detected-types — admin-only (requireAdmin), same-origin JSON, rate-limited (attachments.detectionBackfill), attachment-family private response headers. One bounded pass per call:

  • Scans ready attachment things whose crystal is application/octet-stream with no detectedContentType, in shareId order (cursor pagination).
  • Fetches the object's first 8 KiB from private S3 (AttachmentS3.detectContentType) and runs detectedAttachmentType — the exact code path completion uses (attachments.ts complete()).
  • Browser-playable containers flip to their inline contentType/mediaKind (4-key crystal → renders inline); other canonical sniffed types gain detectedContentType (5-key-with-octet crystal → labeled download row); undetectable bytes stay untouched — deliberately, so a later pass under a wider detector can still claim them.
  • Names, byte sizes, object keys, and object versions never change; the crystal's JSON-payload byte delta is settled through the same transactional storage ledger markReady uses.
  • Ready rows missing objectVersionId adopt the exact current version first via the same HEAD-verify + setObjectVersionId recovery download/completion use (dry runs only read it).

Bounded + safe to re-run: at most 200 rows per pass, 5 workers, 25-second wall-clock budget (mirrors reapExpired); dryRun: true writes nothing; upgraded rows leave the candidate set so real passes converge; nextCursor/hasMore walk the full backlog (required for dry runs); per-row races lose cleanly — the write is fenced on ready state, object version, updatedAt, and sizeBytes.

Store additions: listReadyUndetected + upgradeReadyCrystal (modeled on markReady; name/size immutable so object-byte accounting cannot move; conflicts throw AttachmentStoreConflictError).

Registered in all three places (route file, import map in server/routes/api/[...].ts, apiEndpointDocs entry) — the docs entry IS the Nitro route registration and auto-generates the two -docs smoke tests.

Why an endpoint, not a migration

The admin migrations framework is collection-schemaVersion-shaped (fromVersion/toVersion, global lease, no S3 dependency); this backfill never bumps schemaVersion and needs S3 access, cursor pagination, and dry-run reporting. The attachments family already has the service dependency injection, response conventions, and the cleanup maintenance-endpoint precedent, so the sweep lives there.

Testing

  • pnpm run test:attachments — 123 tests pass, including 5 new ones mirroring the completion-test mock patterns:
    • publishes exactly what completion would (QuickTime inline flip, AVI opaque label, undetectable untouched, name/size preserved, idempotent zeros once drained)
    • dry run counts without writing and pages with a cursor
    • adopts missing object versions, counts gone objects (missingObject) and losing races (conflicts)
    • input validation, custom-Mongo fail-closed, storage_unconfigured 503 mapping, wall-clock budget stop
    • route test: 401 anonymous / 403 non-admin / 405 / 415 / cross-origin 403 / 429 / body passthrough / private headers
  • pnpm run test:rate-limit and the full test:unit battery pass.
  • Live smoke against the worktree Nitro: -docs 200, GET 405, anonymous POST 401, non-admin 403, admin {"dryRun":true} → 200 zero report, unknown body key → 400.
  • TESTING.md gained a backfill checklist line under the video-sniffing section.

Ops notes

  • Run on the canonical deployment: fails closed (400) under a custom MongoDB endpoint; 503 storage_unconfigured without the private-S3 env.
  • Suggested rollout: POST {"dryRun": true} to count, then POST {} repeatedly following nextCursor while hasMore is true. Watch upgradedInline / labeledOpaque vs undetected / missingObject / conflicts / failed.

🤖 Generated with Claude Code

…tals

Ready attachments finalized before PR #319's magic-byte detection keep
crystal contentType application/octet-stream with no detectedContentType,
so browser-playable uploads still render as file cards. Add an admin-only
POST /api/v1/attachments/backfill-detected-types that re-runs detection
from the object's first 8 KiB and publishes exactly what completion would
have: browser-playable containers flip to their inline contentType and
mediaKind, other canonical sniffed types gain detectedContentType, and
undetectable bytes stay untouched so a later, wider detector can claim
them. Names, sizes, object keys, and object versions never change; the
crystal's JSON-payload delta settles through the same transactional
storage ledger markReady uses.

Each pass is bounded (at most 200 rows, 5 workers, 25s wall clock),
idempotent, cursor-paginated, and supports dryRun. New store methods
listReadyUndetected and upgradeReadyCrystal (markReady-modeled, name and
size immutable, object-version-fenced); rate key
attachments.detectionBackfill; route + import map + apiDocs registration;
service and route tests mirroring the completion-test mocks; TESTING.md
checklist line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview Aug 19, 2026 3:29am
thingtime (develop) Ready Ready Preview Aug 19, 2026 3:29am

Request Review

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

✅ Develop S3 preview ready

The alias passed the develop bucket CORS preflight and a final live PR/SHA fence.

Generic Vercel Preview deployments use the shared development runtime; this controller adds the stable exact-SHA alias and marker-scoped cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant