Skip to content

feat: per-file metadata — PR/issue context, custom tags, and search#157

Merged
Zach Dunn (zachdunn) merged 24 commits into
mainfrom
claude/file-metadata-search-5596a4
Jul 14, 2026
Merged

feat: per-file metadata — PR/issue context, custom tags, and search#157
Zach Dunn (zachdunn) merged 24 commits into
mainfrom
claude/file-metadata-search-5596a4

Conversation

@zachdunn

@zachdunn Zach Dunn (zachdunn) commented Jul 13, 2026

Copy link
Copy Markdown
Member

In plain terms

Files now carry searchable key-value metadata. uploads attach records which PR or issue a screenshot belongs to, you can tag any upload with your own pairs (app, url, device, resolution, …), the /f/ file pages show that context (including an "Attached to owner/repo#123" link), and you can search by it — "show me every screenshot attached to PR 123" is now uploads find gh.number=123.

What it does

  • New D1 file_metadata table (one row per pair, indexed for equality search) — the queryable-tag tier, modeled on S3 object tags / Azure blob index tags. R2 custom metadata (provenance, visibility) is untouched.
  • Capture at upload via the existing X-Uploads-Meta-<key> header convention: non-provenance keys become metadata rows instead of being silently dropped; invalid keys/values now reject with typed errors.
  • Limits: keys ^[a-z][a-z0-9._-]{0,63}$, values ≤512 printable chars, ≤24 keys per file/request, ≤8 KB total. Reserved keys: content-sha256, visibility (would shadow server-controlled values).
  • GET/PATCH /v1/:ws/files/:key/metadata (merge semantics, delete list) and GET /v1/:ws/files?meta.<k>=<v> (repeatable, ANDed, combines with prefix/limit).
  • uploads attach writes gh.repo / gh.kind / gh.number / gh.ref automatically (canonical lowercase). New CLI: put/attach --meta k=v, meta get|set, ls --meta, and uploads find k=v ….
  • Both MCP surfaces get parity: metadata on put, plus set_metadata / find_files tools.
  • /f/ pages render the attachment link + a metadata list (public files only; private files still 401 before metadata is fetched).
  • One-time backfill script seeds gh.* for existing gh/<owner>/<repo>/… keys.

What it is not

  • No web search UI yet (API/CLI search only).
  • Presigned uploads still bypass metadata capture (known gap, future /v1/sign work).
  • Re-upload semantics: a PUT with custom --meta headers replaces the file's whole metadata set; without them the existing metadata is preserved (same as the MCP tools). Use uploads meta set to edit individual keys.

How to try it

uploads put ./shot.png --meta app=web --meta device=mobile
uploads attach ./after.png --pr 123
uploads find gh.number=123
uploads meta set screenshots/shot.png page=/settings

Technical notes

Single validation choke point (apps/api/src/file-metadata.ts) covers upload capture, PATCH, and both MCP workers. LIKE prefix search escapes %/_ with an ESCAPE clause. Metadata deletion cascades from object deletes. meta.* filter count capped at 24. Backfill: node apps/api/scripts/backfill-gh-metadata.mjs --dry-run (see docs/ops.md).

Follow-ups deliberately deferred: set_metadata's bare-Error 404 in the remote worker, client-side reserved-key list omits visibility (server rejects with a clear 400), getFileMetadata doesn't wrap raw D1 errors, visibility annotation absent from metadata-filtered listings (documented caveat).

Test plan

  • pnpm --filter @uploads/api test (329 passing, incl. node:sqlite-backed metadata suite)
  • pnpm --filter @uploads/mcp test (32) · pnpm --filter @buildinternet/uploads test (261) · pnpm --filter @uploads/errors test (15) · web (63)
  • pnpm check and pnpm typecheck clean
  • Run the backfill against prod (--dry-run first) after merge

Summary by CodeRabbit

  • New Features

    • Added queryable custom metadata for uploaded files, with validation and size limits.
    • Added CLI support for --meta, metadata retrieval and updates, and metadata-based find and list filtering.
    • Added MCP support for metadata-aware uploads, updates, and file searches.
    • GitHub attachments now include searchable repository, issue/pull request, number, and reference details.
    • Public file pages and API responses can display metadata and GitHub context.
  • Documentation

    • Added CLI usage examples, workflow guidance, and an operational backfill runbook.

Adds the D1 table and file-metadata.ts core module (validation, get/set/
delete, AND-filter search) that later tasks build the metadata CRUD
endpoints and search filter on top of.
PUT splits X-Uploads-Meta-* headers: allowlisted keys still go to R2
provenance unchanged, everything else is validated (file-metadata.ts) and
stored in D1 via setFileMetadata inside putObject. Invalid keys/values or
a >24-key cap breach now reject the upload instead of being silently
dropped. Overwriting a key does a full delete-then-set replace of its
metadata; deleteObject cascades via deleteFileMetadata; dryRun writes
nothing.
splitUploadMetaHeaders pre-filtered empty header values before validation,
reproducing the old silent drop for X-Uploads-Meta-<key> with an empty
value. Empty (and empty-key) custom entries now flow to
validateMetadataEntries so the upload 400s with the typed error; the
allowlisted provenance branch keeps its historical lenience.
Adds GET/PATCH /v1/:workspace/files/:key/metadata on top of Task 1's
file-metadata.ts (caps, merge semantics) and Task 2's put/delete
cascade, so callers can read and update an object's queryable
metadata without re-uploading the file.
GET /v1/:workspace/files accepts repeatable meta.<key>=<value> params,
ANDed, and switches the listing to the D1 file_metadata index instead
of the R2 prefix-list when at least one is present. Combines with
prefix and limit. Invalid keys or a repeated same-key param are
rejected with a ValidationError. No meta.* params leaves the existing
R2 path unchanged.
GET /public/files/:workspace/:key now returns the file's D1 file_metadata
map (omitted when empty) and a derived github object built from gh.repo/
gh.kind/gh.number when all three parse validly. Malformed or incomplete
gh.* pairs just fall back to raw metadata with no github object. The
private-file 401 gate still runs before the metadata fetch, so nothing
leaks on private objects.
A client could store a D1 file_metadata row named content-sha256 (via
upload headers or the metadata PATCH), shadowing the server-computed
integrity hash in R2 provenance. Server-set provenance keys are now a
named constant (PROVENANCE_SERVER_KEYS) and validateMetadataEntries —
the shared choke point for upload capture, PATCH, and future callers —
rejects them with file_metadata_reserved_key. gh.* keys stay writable
(system-managed by convention per the design doc).
Task 6: render the "Attached to" GitHub PR/issue row and a generic
Metadata section on the /f/ file page, consuming Task 5's github/metadata
fields from GET /public/files/:workspace/:key.
- put --meta k=v (repeatable, first-= split) validated client-side and sent
  as more X-Uploads-Meta-<key> headers alongside provenance
- attach writes gh.repo/gh.kind/gh.number/gh.ref automatically from its
  resolved target; --meta extras merge in, target pairs win on collision
- new meta get/set commands and list --meta / find k=v filter alias
- client gains getMetadata/patchMetadata/findFiles; cli-args flags now
  support repeatable string flags via flagValues
MCP parity for per-file metadata (Task 8). Remote worker's put tool and the
local stdio MCP's put/attach tools gain a metadata param; attach keeps
auto-injecting gh.* via the shared command-layer helper. New set_metadata
and find_files tools mirror the CLI's meta set / find commands.
…ics, filter cap, reserved keys)

Consolidated fix wave ahead of the per-file metadata PR: escape SQL LIKE
wildcards in findObjectsByMetadata's prefix so `_`/`%` in a prefix match
literally; make the REST PUT route pass metadata: undefined (preserve)
instead of always full-replacing when a re-PUT carries no custom
X-Uploads-Meta-* headers; cap GET list's meta.* filter params at
META_MAX_KEYS with a typed error; register the file_metadata_duplicate_filter
and file_metadata_too_many_filters error codes; reserve `visibility` as a
custom metadata key so it can't shadow the real R2 visibility gate.
Mirrors the local stdio MCP's set_metadata/find_files tools on the remote
worker (apps/mcp/src/tools.ts), implemented against the shared api modules
(setFileMetadata/findObjectsByMetadata) so both MCP surfaces expose the
same metadata CRUD/search behavior called out in the design doc.
Adds the re-upload metadata contract (--meta replaces, omitted preserves;
attach always replaces since it always sends gh.*) to the CLI's put/attach
--help text and the uploads-cli skill doc, per fix 7 of the metadata
final-review pass.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (2)
  • coderabbit:review
  • review
🚫 Excluded labels (none allowed) (1)
  • wip

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa8c26cd-ae33-4581-8970-5448e50d39d0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change adds queryable per-file metadata across the API, uploads client, CLI, MCP tools, and public file views. It introduces D1 persistence, validation and filtering, GitHub metadata injection and backfill, metadata management endpoints, search commands, derived public GitHub context, and comprehensive tests and documentation.

Changes

Queryable file metadata

Layer / File(s) Summary
Metadata storage and contracts
apps/api/migrations/*, apps/api/src/file-metadata.ts, apps/api/src/files-core.ts, packages/errors/src/codes.ts
Adds validated D1-backed metadata with merge, replace, delete, and AND-filtered lookup semantics, integrated with object lifecycle operations.
API routes and GitHub backfill
apps/api/src/provenance.ts, apps/api/src/routes/files.ts, apps/api/src/routes/public-files.ts, apps/api/scripts/backfill-gh-metadata.*
Adds metadata header splitting, metadata GET/PATCH routes, metadata-aware listing, public metadata responses, derived GitHub context, and a paginated GitHub metadata backfill script.
CLI and uploads client workflows
packages/uploads/src/cli-args.ts, packages/uploads/src/client.ts, packages/uploads/src/commands.ts, packages/uploads/src/metadata.ts, packages/uploads/src/github.ts
Adds repeatable --meta, metadata client methods, find, meta get, meta set, metadata filtering, and automatic gh.* metadata for attachments.
MCP metadata tools
packages/uploads/src/mcp/*, packages/uploads/src/mcp/tools.ts, apps/mcp/src/tools.ts
Adds metadata inputs for upload and attach tools plus set_metadata and find_files.
Public file model and rendering
apps/web/src/lib/public-file.ts, apps/web/src/pages/f/[workspace]/[...key].astro
Adds validated metadata and GitHub context to public file payloads and renders attachment and custom metadata sections.
Validation, tests, and rollout support
apps/api/test/*, apps/mcp/test/*, packages/uploads/test/*, apps/web/src/lib/public-file.test.ts, docs/*, skills/uploads-cli/*, .changeset/*
Adds coverage for persistence, validation, routes, CLI, client, MCP, public responses, backfill behavior, documentation, and the package release note.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: coderabbit:review

Poem

I’m a rabbit tagging files in the night,
With gh.* carrots tucked tidy and bright.
Find, set, and attach—what a wonderful spree,
Metadata hops through the API tree.
D1 keeps each tiny label in sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: per-file metadata with PR/issue context, custom tags, and search.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/file-metadata-search-5596a4

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 13, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-auth a328f3c Commit Preview URL

Branch Preview URL
Jul 14 2026, 12:51 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 13, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-api a328f3c Commit Preview URL

Branch Preview URL
Jul 14 2026, 12:51 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 13, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
uploads-web c0659af Commit Preview URL

Branch Preview URL
Jul 14 2026, 12:05 AM

…le-metadata

- files.ts: keep metadata imports + adopt objectPublicUrls; meta-filtered
  listings now return embedUrl like the plain list path
- SKILL.md: keep both post-embed paragraphs (url durability + attach metadata)
- apps/mcp/tsconfig: include ../api/src/env.d.ts so Env augmentations
  (EMBED_PUBLIC_BASE_URL, #154) type-check from the shared api source
- routes-files tests: expect embedUrl on meta-filtered items
Consolidate three copy-pasted node:sqlite fake-D1 harnesses
(file-metadata-sqlite/galleries-sqlite) into apps/api/test/helpers/sqlite-d1.ts
parameterized by migration path(s) and optional pragmas, and consolidate the
three near-identical hand-rolled file_metadata D1 mocks in routes-files.test.ts,
routes-public-files.test.ts, and usage-fake-d1.ts into a shared
FileMetadataTable helper that each suite's fake D1 tries first, falling back
to its own auth_tokens/workspace_usage logic. No behavior change.
…ndependent awaits

Promote optStringArray/METADATA_DESCRIPTION/metadataProp from duplicated
copies in apps/mcp/src/tools.ts and packages/uploads/src/mcp/tools.ts into
packages/uploads/src/mcp/args.ts (re-exported from server.ts), extract a
shared validateMetadataFilters (count cap + key format) in
apps/api/src/file-metadata.ts used by both the REST list endpoint and the
MCP find_files tool, run the independent storageConfig/findObjectsByMetadata
awaits in Promise.all on both surfaces, and make the CLI's validateMetaMap
validate entries directly instead of round-tripping through parseMetaFlags's
"k=v" string reconstruction. Also adds "visibility" to the CLI's reserved
metadata keys to match the server, with a test.
…oad path

putObject's full-replace path called deleteFileMetadata then setFileMetadata,
which re-reads the now-guaranteed-empty map and re-validates before writing.
Add replaceFileMetadata (one validateMetadataEntries call, then a single
db.batch delete-all + upserts) and use it from putObject, making delete+set
atomic in one batch as a side effect. setFileMetadata's merge/PATCH path is
unchanged. Adds sqlite-backed tests for replace/clear/cap-rejection/atomicity.
@zachdunn

Copy link
Copy Markdown
Member Author

CodeRabbit (@coderabbitai) review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/files-core.ts (1)

194-206: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Partial write can skip usage accounting at apps/api/src/files-core.ts:194-206. store.upload can succeed, replaceFileMetadata can fail, and recordUsageSafe will never run, leaving the object stored but the ledger under-counted and metadata rows missing. Consider moving usage accounting before the metadata batch or adding compensation on failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/files-core.ts` around lines 194 - 206, The upload path can
persist the object but skip usage accounting when replaceFileMetadata fails.
Update the flow around replaceFileMetadata and recordUsageSafe so usage
accounting runs even if metadata replacement fails, while preserving the
metadata update and existing usage values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/pages/f/`[workspace]/[...key].astro:
- Around line 43-49: The metadataEntries rendering in
apps/web/src/pages/f/[workspace]/[...key].astro lines 43-49 requires no code
change; retain the existing behavior. Update the “Custom metadata & search”
section in skills/uploads-cli/SKILL.md lines 210-235 to explicitly warn that
non-`gh.*` values supplied through `--meta` or MCP metadata are displayed on the
object’s public file page and must not contain sensitive internal notes, IDs, or
paths.

In `@packages/uploads/src/commands.ts`:
- Around line 916-930: Update runFindFiles to detect when --cursor is supplied
and reject it explicitly before calling ctx.client.findFiles, returning the
command’s established error status and message format. Preserve the existing
metadata filtering, prefix, limit, output handling, and successful return
behavior when --cursor is absent.

In `@packages/uploads/src/mcp/args.ts`:
- Around line 37-40: Update the result accumulator in the argument-validation
function to preserve an own "__proto__" key instead of invoking
Object.prototype’s setter, ensuring invalid metadata reaches validation and is
rejected. Add a regression test covering {"__proto__":"x"} and assert it fails
metadata-key validation.

In `@packages/uploads/src/mcp/tools.ts`:
- Around line 581-583: Update the metadata handling in the attachment flow to
merge optStringRecord(args, "metadata") with ghMetadataFromTarget(target) first,
then validate the complete metadata object with validateMetaMap before any file
reads; remove the validation that only checks metaExtras.

---

Outside diff comments:
In `@apps/api/src/files-core.ts`:
- Around line 194-206: The upload path can persist the object but skip usage
accounting when replaceFileMetadata fails. Update the flow around
replaceFileMetadata and recordUsageSafe so usage accounting runs even if
metadata replacement fails, while preserving the metadata update and existing
usage values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e76c5314-bff4-47ae-aa9c-8483d12115b8

📥 Commits

Reviewing files that changed from the base of the PR and between c5b36a3 and 1ee73ff.

📒 Files selected for processing (50)
  • .changeset/file-metadata.md
  • apps/api/migrations/20260713210559_file_metadata.sql
  • apps/api/package.json
  • apps/api/scripts/backfill-gh-metadata.d.mts
  • apps/api/scripts/backfill-gh-metadata.mjs
  • apps/api/src/file-metadata.ts
  • apps/api/src/files-core.ts
  • apps/api/src/provenance.ts
  • apps/api/src/routes/files.ts
  • apps/api/src/routes/public-files.ts
  • apps/api/test/backfill-gh-metadata.test.ts
  • apps/api/test/file-metadata-sqlite.test.ts
  • apps/api/test/file-metadata.test.ts
  • apps/api/test/galleries-sqlite.test.ts
  • apps/api/test/helpers/fake-file-metadata-table.ts
  • apps/api/test/helpers/sqlite-d1.ts
  • apps/api/test/routes-files.test.ts
  • apps/api/test/routes-galleries.test.ts
  • apps/api/test/routes-key-policy.test.ts
  • apps/api/test/routes-public-files.test.ts
  • apps/api/test/usage-fake-d1.ts
  • apps/mcp/src/tools.ts
  • apps/mcp/test/mcp.test.ts
  • apps/mcp/tsconfig.json
  • apps/web/src/lib/public-file.test.ts
  • apps/web/src/lib/public-file.ts
  • apps/web/src/pages/f/[workspace]/[...key].astro
  • docs/ops.md
  • packages/errors/src/codes.ts
  • packages/uploads/README.md
  • packages/uploads/src/cli-args.ts
  • packages/uploads/src/cli.ts
  • packages/uploads/src/client.ts
  • packages/uploads/src/commands.ts
  • packages/uploads/src/github.ts
  • packages/uploads/src/index.ts
  • packages/uploads/src/mcp/args.ts
  • packages/uploads/src/mcp/server.ts
  • packages/uploads/src/mcp/tools.ts
  • packages/uploads/src/metadata.ts
  • packages/uploads/test/cli-args.test.ts
  • packages/uploads/test/client-metadata.test.ts
  • packages/uploads/test/commands-attach.test.ts
  • packages/uploads/test/commands-find.test.ts
  • packages/uploads/test/commands-list.test.ts
  • packages/uploads/test/commands-meta.test.ts
  • packages/uploads/test/commands-put.test.ts
  • packages/uploads/test/mcp.test.ts
  • packages/uploads/test/metadata.test.ts
  • skills/uploads-cli/SKILL.md

Comment thread apps/web/src/pages/f/[workspace]/[...key].astro
Comment thread packages/uploads/src/commands.ts
Comment thread packages/uploads/src/mcp/args.ts Outdated
Comment thread packages/uploads/src/mcp/tools.ts
- SKILL.md: warn that non-gh.* --meta/MCP metadata values render publicly
  on the /f/ file page.
- CLI: reject --cursor with metadata search (list --meta / find), which
  previously accepted and silently ignored it.
- mcp/args.ts optStringRecord: use a null-prototype accumulator so a
  __proto__ metadata key can't be silently dropped by the inherited setter.
- mcp/tools.ts attach + commands.ts runAttach: validate the merged
  metadata (extras + gh.* pairs) against the key/byte caps, not just the
  extras alone, so the merge can't silently exceed the cap server-side.
- files-core.ts putObject: record usage before writing custom metadata, so
  a failing metadata batch no longer leaves the ledger under-counted.
@zachdunn Zach Dunn (zachdunn) merged commit 46b6860 into main Jul 14, 2026
5 checks passed
@zachdunn Zach Dunn (zachdunn) deleted the claude/file-metadata-search-5596a4 branch July 14, 2026 00:54
@zachdunn

Copy link
Copy Markdown
Member Author

Shipped and verified in production: route hotfix in #158, gh backfill run against the default workspace (40/40 patched), search and file-page attachment context confirmed live. Deferred follow-ups are tracked in #159.

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