feat: Add files upload / storage support#703
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a pluggable storage adapter system for VitNode, enabling file uploads to be stored locally on disk or via cloud providers like AWS S3/Cloudflare R2 and Supabase Storage. It adds the core StorageModel with file validation and image optimization using sharp, database schemas for tracking files, and an admin panel interface to test the storage configuration. The review feedback highlights several opportunities to improve robustness, such as safely handling dynamic imports of sharp to avoid masking 500 errors as 400s, sanitizing slashes in URL construction, safely stripping path prefixes in static file serving, and utilizing optional chaining on context lookups to prevent potential runtime crashes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private async optimizeImage(body: Buffer, mimeType: string): Promise<Buffer> { | ||
| const image = this.c.get("core").storage?.image; | ||
| if (!image || !PROCESSABLE_IMAGE_MIME_TYPES.has(mimeType)) { | ||
| return body; | ||
| } | ||
|
|
||
| const quality = image.quality ?? DEFAULT_IMAGE_QUALITY; | ||
|
|
||
| try { | ||
| const { default: sharp } = await import("sharp"); | ||
| const format = (await sharp(body).metadata()).format; | ||
| if (!format) { | ||
| return body; | ||
| } | ||
|
|
||
| return await sharp(body).toFormat(format, { quality }).toBuffer(); | ||
| } catch { | ||
| throw new HTTPException(400, { | ||
| message: "Invalid or corrupt image file", | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
The optimizeImage method imports sharp dynamically inside a single try-catch block that catches all errors and throws a 400 Bad Request with "Invalid or corrupt image file".
If sharp fails to load (e.g., due to missing native binaries or installation issues on the target platform), this system-level error (which should be a 500 Internal Server Error) is incorrectly masked as a client-side validation error.
Separating the dynamic import from the image processing logic allows you to throw a proper 500 error if the dependency fails to load, while keeping the 400 error for actual corrupt image files. Additionally, we should use optional chaining on this.c.get("core") to prevent potential runtime crashes if the core context is not yet initialized.
private async optimizeImage(body: Buffer, mimeType: string): Promise<Buffer> {
const image = this.c.get("core")?.storage?.image;
if (!image || !PROCESSABLE_IMAGE_MIME_TYPES.has(mimeType)) {
return body;
}
const quality = image.quality ?? DEFAULT_IMAGE_QUALITY;
let sharp;
try {
const { default: s } = await import("sharp");
sharp = s;
} catch (err) {
throw new HTTPException(500, {
message: "Image optimization library (sharp) failed to load",
});
}
try {
const format = (await sharp(body).metadata()).format;
if (!format) {
return body;
}
return await sharp(body).toFormat(format, { quality }).toBuffer();
} catch {
throw new HTTPException(400, {
message: "Invalid or corrupt image file",
});
}
}| const getUrl = (key: string): string => | ||
| `${baseUrl ?? CONFIG.api.origin}${publicPath}/${key}`; |
There was a problem hiding this comment.
URL construction using simple string interpolation can easily result in double slashes (if baseUrl has a trailing slash) or missing slashes (if publicPath doesn't have a leading slash).
It is safer to sanitize the slashes when constructing the URL to ensure it is always valid.
| const getUrl = (key: string): string => | |
| `${baseUrl ?? CONFIG.api.origin}${publicPath}/${key}`; | |
| const getUrl = (key: string): string => { | |
| const base = (baseUrl ?? CONFIG.api.origin).replace(/\/$/, ""); | |
| const path = publicPath.replace(/^\/|\/$/g, ""); | |
| return `${base}/${path}/${key}`; | |
| }; |
| staticStorage.mountPath, | ||
| serveStatic({ | ||
| root: staticStorage.root, | ||
| rewriteRequestPath: path => path.replace(staticStorage.stripPrefix, ""), |
There was a problem hiding this comment.
path.replace(staticStorage.stripPrefix, "") replaces the first occurrence of stripPrefix anywhere in the path. If the prefix happens to be a substring of a folder or file name later in the path, it will corrupt the path.
It is much safer to only strip the prefix if the path starts with it.
rewriteRequestPath: path =>
path.startsWith(staticStorage.stripPrefix)
? path.slice(staticStorage.stripPrefix.length)
: path,| } | ||
|
|
||
| private requireProvider(): StorageApiPlugin { | ||
| const provider = this.c.get("core").storage?.adapter; |
There was a problem hiding this comment.
this.c.get("core") is accessed directly without optional chaining. If the global middleware hasn't run or if core is not set on the context for any reason, this will throw a TypeError: Cannot read properties of undefined (reading 'storage').
Using optional chaining is safer and prevents potential runtime crashes.
| const provider = this.c.get("core").storage?.adapter; | |
| const provider = this.c.get("core")?.storage?.adapter; |
Improving Documentation
pnpm lint:fixto fix formatting issues before opening the PR.Description
What?
Why?