Skip to content

fix(files): download a markdown file as a zip only when it really has assets - #7009

Merged
waleedlatif1 merged 4 commits into
stagingfrom
fix/file-zip-download
Aug 23, 2026
Merged

fix(files): download a markdown file as a zip only when it really has assets#7009
waleedlatif1 merged 4 commits into
stagingfrom
fix/file-zip-download

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Downloading a markdown file returned a zip with an empty assets/ folder whenever the document merely mentioned an embed URL. Any document about the files API tripped it — an inline `/api/files/view/{id}` in prose was enough.
  • Two independent causes, either of which reproduced it on its own:
    • The detector regex-scanned raw document text, so prose, inline code spans, fenced samples, and links all counted as embedded assets.
    • The export route picked zip-vs-markdown from the candidate count before resolving anything, so embeds that were missing, unreadable, or oversized still produced a zip.
  • Detection now uses the markdown lexer and counts only real image embeds (![alt](src), reference images, and <img> tags), so a mention can't masquerade as an attachment. Links are excluded — a link is navigated to, not displayed.
  • The export format is now decided from what actually got bundled, so a document whose embeds don't resolve downloads as the document itself.
  • Moved the document scan out of the copilot tool tree into lib/uploads/server/, where both consuming file routes already live, and dropped two pass-through wrappers. The <img> src reader is now shared between the clipboard handlers and the scan instead of being defined twice.
  • Walked tokens explicitly rather than via marked's walkTokens, which concatenates its callback's return value once per token and so costs O(n²) — a 254KB document measured 5.4s of blocked event loop versus 14ms. That path is reachable by anonymous public-share traffic, so this also removes an unauthenticated stall vector.
  • The public share's referenced-by-doc gate tightens as a side effect: a file the document only links to or names in prose is no longer served through the share token. It was already bounded to the document's own workspace, and the viewer only ever requests embedded images, so nothing legitimate loses access.

Type of Change

  • Bug fix

Testing

  • New regression tests cover the reported case: a document that only mentions embed URLs, and a document whose embeds resolve to nothing. Each was verified to fail against the previous behavior and pass now.
  • Also covered: reference-style images, inline and block <img>, and that fenced/<pre> content is ignored.
  • bun run type-check, bun run lint, bun run check:api-validation, and the full bun run check:audits (33 audits) pass.
  • 3,322 tests across the affected areas pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

… assets

A document that merely mentions an embed URL in prose or an inline code span
counted as having attachments, so any document about the files API downloaded
as a zip whose assets/ folder was empty.

- Detect embeds with the markdown lexer instead of scanning raw text, so only
  real image embeds count: prose, code spans, fenced samples, and links no
  longer do
- Choose the export format after resolving assets rather than from the
  candidate count, so a missing, unreadable, or oversized embed falls back to
  the plain document instead of an empty zip
- Move the document scan out of the copilot tool tree into lib/uploads/server,
  where both file routes already live, and drop two pass-through wrappers
- Share one <img> src reader between the clipboard handlers and the scan
- Walk tokens explicitly: marked's walkTokens concatenates per token and costs
  O(n^2), measuring 5.4s on a 254KB document against 14ms here, on a path
  anonymous public-share traffic reaches
@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 23, 2026 5:50pm

Request Review

@cursor

cursor Bot commented Aug 23, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches public-share authorization (referenced-by-doc) and export bundling. The gate is tighter (mentions/links no longer cascade), which is the intended security fix, but a parser bug could miss or over-count embeds.

Overview
Markdown downloads no longer become empty-assets/ zips when a document only mentions file URLs, or when referenced images fail to resolve.

Embed detection now lexes markdown and counts only real images (![alt](src), reference images, <img>), not prose, code, or links. Export format follows what actually bundled: missing/unreadable embeds download as the .md itself. Percent-encoded ids stay spelled as in the document for rewrite, and are decoded only for metadata lookup.

The scan moves to lib/uploads/server (own Marked instance, linear token walk). Public-share inline serving uses the same grammar, so a share token no longer cascades to files the document merely links to or names.

Reviewed by Cursor Bugbot for commit f346f5c. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes Markdown export format depend on successfully bundled image assets and limits image-reference detection to actual Markdown or HTML image embeds.

  • Moves document-level embedded-image extraction into the uploads server layer.
  • Preserves source-spelled IDs for Markdown rewriting while decoding IDs for storage lookup.
  • Reuses the shared HTML image-source parser across server and editor paths.
  • Tightens public-share image authorization to files actually embedded by the shared document.
  • Adds regression coverage for unresolved assets, encoded IDs, reference images, HTML images, and non-image URL mentions.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/uploads/server/embedded-image-refs.ts Introduces bounded, lexer-based extraction of actual Markdown and HTML image references while preserving separate document and storage ID representations.
apps/sim/app/api/files/export/[id]/route.ts Resolves document-spelled IDs through their stored form and returns plain Markdown when no image asset is successfully bundled.
apps/sim/app/api/files/public/[token]/inline/route.ts Reuses the shared image-aware extraction result for the public share’s referenced-by-document authorization gate.
apps/sim/lib/uploads/utils/embedded-image-ref.ts Keeps single-source reference parsing isomorphic and centralizes raw HTML image-source extraction for client and server consumers.
apps/sim/app/api/files/export/[id]/route.test.ts Adds regression coverage for encoded IDs, unresolved or unreadable assets, and Markdown-versus-ZIP response selection.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Markdown document] --> B[Lex image tokens]
  B --> C[Extract source-spelled IDs and decoded keys]
  C --> D[Resolve stored file metadata]
  D --> E{Any assets bundled?}
  E -->|No| F[Return Markdown]
  E -->|Yes| G[Rewrite embedded URLs]
  G --> H[Return ZIP with assets]
  C --> I[Public-share referenced-image gate]
  I --> J[Workspace and content checks]
  J --> K[Serve inline image]
Loading

Reviews (6): Last reviewed commit: "fix(files): resolve an embed by its stor..." | Re-trigger Greptile

Comment thread apps/sim/lib/uploads/utils/embedded-image-ref.ts Outdated
Decoding the id let a percent-encoded embed resolve and bundle its asset while
the rewrite, which searches the document for that id, found nothing — the zip
kept an API URL that renders as a broken image offline. Keys stay decoded;
they are matched against stored keys, not against document text.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 10bb0e7. Configure here.

…spelling

An embed carries two representations and they are not interchangeable: metadata
resolves by the stored id, while the rewrite finds the embed by searching the
document for the spelling it used. Using one for both either drops a
percent-encoded asset or bundles it behind a link still pointing at the API.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Addressed the round-2 summary finding in f346f5c.

The observation was correct: preserving the spelling fixed the rewrite but moved the problem to the lookup, so a percent-encoded embed resolved to nothing and was dropped. That was fail-safe — the asset simply isn't bundled and the markdown keeps its original working URL — but it was still a trade rather than a fix.

An embed carries two representations and they aren't interchangeable:

  • stored id — what getFileMetadataById matches against
  • document spelling — what the rewrite searches the markdown for

Collapsing them in either direction breaks one side. The export now decodes for the lookup and keeps the spelling for the rewrite, so a percent-encoded embed both resolves and gets rewritten. Covered by a regression test that bundles the asset and asserts the markdown ends up with ./assets/... and no remaining /api/files/view/ reference; verified it fails without the change.

Worth noting for scope: no producer in the system emits encoded ids — the file agent and the editor both write them plainly, and ids are [A-Za-z0-9_-]. So this is a robustness fix on an input that shouldn't occur, not a live defect.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/files/public/[token]/inline/route.ts

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f346f5c. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f346f5c. Configure here.

Comment thread apps/sim/lib/copilot/tools/server/files/embedded-image-refs.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

…om a document

The export bundler decoded an embed's spelling before looking it up, but the
file-agent's embeddability warning did not, so a percent-encoded embed the
export resolves and bundles could still be reported as one that will not
survive an export. Both now share one helper.

Request-supplied ids are untouched: their route contracts already constrain
them to the plain id charset, so there is no spelling to decode.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
waleedlatif1 merged commit b44d285 into staging Aug 23, 2026
29 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/file-zip-download branch August 23, 2026 18:15
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