Skip to content

fix(core): show assets stored in nested folders in the asset library#240

Open
adawang1210 wants to merge 1 commit into
1weiho:mainfrom
adawang1210:fix/asset-library-nested-folders
Open

fix(core): show assets stored in nested folders in the asset library#240
adawang1210 wants to merge 1 commit into
1weiho:mainfrom
adawang1210:fix/asset-library-nested-folders

Conversation

@adawang1210

@adawang1210 adawang1210 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Problem

Fixes #224. Files placed in subfolders of a slide's assets/ (or the global assets dir) never appear in the asset library — only top-level files are listed.

Cause

In packages/core/src/vite/routes/assets.ts, the list handler read the scoped assets directory with a single non-recursive fs.readdir, and validateAssetName rejected any name containing a path separator. Nested files were therefore never enumerated, never served, and never matched for usage detection.

Fix

  • Add listAssetFilesRecursive, which walks the scoped directory and returns forward-slash relative paths.
  • Add validateAssetPath (nested-aware sibling of validateAssetName): validates every segment, requires an extension only on the final segment, and rejects .., absolute, and backslash paths. The existing path.resolve(...) + startsWith(dir + sep) containment check remains the traversal boundary.
  • Support nested paths end to end: list, serve, usages, upload (creates parent dirs), rename (keeps the asset in its directory), and delete. URLs keep the nested name in a single path segment via encodeURIComponent (/%2F), so routing is unchanged.

Tests

Added unit tests for validateAssetPath (nested accept, traversal/absolute/backslash/hidden-segment reject) and listAssetFilesRecursive (nested enumeration, missing dir). pnpm test, pnpm core typecheck, and biome check on the changed files all pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • The asset library now supports and displays assets organized in nested folders. Assets can be accessed using paths like logos/brand.svg. Upload and rename operations preserve nested folder structures, enabling improved asset organization and discoverability.

The asset list endpoint read the scoped assets dir non-recursively, so
files inside subfolders never appeared. Walk the directory recursively and
support forward-slash nested paths end to end (list, serve, usages, upload,
rename, delete), keeping the path-traversal containment guard. Rename keeps
a nested asset in its directory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

@adawang1210 is attempting to deploy a commit to the Yiwei Ho Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds validateAssetPath and listAssetFilesRecursive to the core assets module, enabling nested forward-slash paths to be validated and directories to be enumerated recursively. Asset routes are updated to use these new functions for listing, usages lookup, upload directory creation, and rename path preservation. Tests and a patch changeset entry are included.

Changes

Nested Asset Library Support

Layer / File(s) Summary
Path validation and recursive listing utilities
packages/core/src/files/assets.ts
Adds internal isValidAssetSegment, new exported validateAssetPath (accepts forward-slash nested paths), new exported async listAssetFilesRecursive (returns relative paths or null on ENOENT), and switches resolveAssetFile/resolveScopedAssetFile from validateAssetName to validateAssetPath.
Asset routes wired to new utilities
packages/core/src/vite/routes/assets.ts
Imports validateAssetPath; switches usages route and listing filter to validateAssetPath; replaces fs.readdir with listAssetFilesRecursive; changes upload mkdir target to path.dirname(file); fixes PATCH rename to derive targetPath preserving the source directory and returns it in the success response.
Tests for new utilities and changeset entry
packages/core/src/files/assets.test.ts, .changeset/asset-library-nested-folders.md
Adds validateAssetPath test suite (valid nested paths, extension rules, rejection cases) and listAssetFilesRecursive suite (real temp dir with nested files, non-existent dir returns null); adds patch changeset entry.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 Hopping through folders, deep in the tree,
No file left hidden, not one path unseen!
With slashes and segments checked letter by letter,
Nested assets are found — the library's better.
The rabbit digs deep, past the topsoil and roots,
And returns every file in its forward-slash boots! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% 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: enabling nested folder support in the asset library, which directly addresses the core problem described in the PR objectives.
Linked Issues check ✅ Passed The PR implements all objectives from #224: recursive directory listing via listAssetFilesRecursive, nested-path validation via validateAssetPath, and end-to-end support across list, serve, upload, rename, and delete operations.
Out of Scope Changes check ✅ Passed All changes are directly related to supporting nested assets: validation functions, recursive directory listing, route handler updates, and comprehensive test coverage. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 1

🤖 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 `@packages/core/src/vite/routes/assets.ts`:
- Around line 95-99: The fs.stat() call within the entries loop does not handle
the case where a file may be deleted between the time it was enumerated and when
stat is attempted, causing the entire asset listing request to fail with a 500
error. Wrap the fs.stat() call in a try-catch block to catch ENOENT errors (or
other file system errors) and simply continue to the next entry when such an
error occurs, allowing the listing to complete despite transient file deletions.
🪄 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: c7e14464-a96a-469c-8fcd-8a2a9f44cf18

📥 Commits

Reviewing files that changed from the base of the PR and between 0128ba4 and e68a65e.

📒 Files selected for processing (4)
  • .changeset/asset-library-nested-folders.md
  • packages/core/src/files/assets.test.ts
  • packages/core/src/files/assets.ts
  • packages/core/src/vite/routes/assets.ts

Comment on lines 95 to 99
for (const name of entries) {
if (!validateAssetName(name)) continue;
if (!validateAssetPath(name)) continue;
const stat = await fs.stat(path.join(scopedDir, name));
if (!stat.isFile()) continue;
assets.push({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle per-entry ENOENT during listing to prevent transient 500s.

At Line 97, the file can disappear between recursive enumeration and fs.stat(...); that currently aborts the entire list request instead of skipping the vanished file.

Suggested fix
         for (const name of entries) {
           if (!validateAssetPath(name)) continue;
-          const stat = await fs.stat(path.join(scopedDir, name));
-          if (!stat.isFile()) continue;
-          assets.push({
-            name,
-            size: stat.size,
-            mtime: stat.mtimeMs,
-            mime: mimeForFilename(name),
-            url: `/__assets/${slideId}/${encodeURIComponent(name)}`,
-            unused: true,
-          });
+          try {
+            const stat = await fs.stat(path.join(scopedDir, name));
+            if (!stat.isFile()) continue;
+            assets.push({
+              name,
+              size: stat.size,
+              mtime: stat.mtimeMs,
+              mime: mimeForFilename(name),
+              url: `/__assets/${slideId}/${encodeURIComponent(name)}`,
+              unused: true,
+            });
+          } catch (err) {
+            if ((err as NodeJS.ErrnoException).code === 'ENOENT') continue;
+            throw err;
+          }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const name of entries) {
if (!validateAssetName(name)) continue;
if (!validateAssetPath(name)) continue;
const stat = await fs.stat(path.join(scopedDir, name));
if (!stat.isFile()) continue;
assets.push({
for (const name of entries) {
if (!validateAssetPath(name)) continue;
try {
const stat = await fs.stat(path.join(scopedDir, name));
if (!stat.isFile()) continue;
assets.push({
name,
size: stat.size,
mtime: stat.mtimeMs,
mime: mimeForFilename(name),
url: `/__assets/${slideId}/${encodeURIComponent(name)}`,
unused: true,
});
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') continue;
throw err;
}
}
🤖 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 `@packages/core/src/vite/routes/assets.ts` around lines 95 - 99, The fs.stat()
call within the entries loop does not handle the case where a file may be
deleted between the time it was enumerated and when stat is attempted, causing
the entire asset listing request to fail with a 500 error. Wrap the fs.stat()
call in a try-catch block to catch ENOENT errors (or other file system errors)
and simply continue to the next entry when such an error occurs, allowing the
listing to complete despite transient file deletions.

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.

Asset library does not show files in nested slide asset folders

1 participant