Skip to content

feat: Add files upload / storage support#703

Draft
aXenDeveloper wants to merge 1 commit into
canaryfrom
feat/storage
Draft

feat: Add files upload / storage support#703
aXenDeveloper wants to merge 1 commit into
canaryfrom
feat/storage

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Improving Documentation

Description

What?

Why?

@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
vitnode-docs Error Error Jul 5, 2026 8:56pm

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +77 to +98
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",
});
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

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",
      });
    }
  }

Comment on lines +34 to +35
const getUrl = (key: string): string =>
`${baseUrl ?? CONFIG.api.origin}${publicPath}/${key}`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
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}`;
};

Comment thread apps/api/src/index.ts
staticStorage.mountPath,
serveStatic({
root: staticStorage.root,
rewriteRequestPath: path => path.replace(staticStorage.stripPrefix, ""),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
const provider = this.c.get("core").storage?.adapter;
const provider = this.c.get("core")?.storage?.adapter;

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

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant