#Is this is legit way to upload files? Haven't seen this in any example

7 messages · Page 1 of 1 (latest)

meager kelp
#

With this approach you do not have to create http route.

frontend:

function convertFileToBase64(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result as string);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

  const upload = useAction(api.image.upload);

  const handleUploadLogo = async (file: File | null) => {
    if (!file) return;
    const base64 = await convertFileToBase64(file);
    await upload({ base64, table: "accounts", column: "logo" });
  };
#

backend image.ts

export const upload = authAction({
  args: {
    base64: v.string(),
    table: v.union(v.literal("accounts"), v.literal("products")),
    column: v.string(),
  },
  handler: async (ctx, args) => {
    const rowId = // get row id here or from frontend

    const storageId = await ctx.runAction(internal.image_node.upload, {
      base64: args.base64,
    });

    if (!storageId) return;

    await ctx.runMutation(internal.image.saveImage, {
      storageId,
      table: args.table,
      column: args.column,
      rowId,
    });
  },
});

export const saveImage = internalMutation({
  args: {
    storageId: v.id("_storage"),
    table: v.union(v.literal("accounts"), v.literal("products")),
    column: v.string(),
    rowId: v.id("accounts"),
  },
  handler: async (ctx, args) => {
    const entity = await ctx.table(args.table).getX(args.rowId);

    const deleteStorageId = () => {
      // @ts-expect-error
      const previousStorageId = entity[args.column] as Id<"_storage"> | null;
      if (!previousStorageId) return;
      return ctx.storage.delete(previousStorageId);
    };

    await Promise.all([entity.patch({ [args.column]: args.storageId }), deleteStorageId()]);
  },
});
#

backend image_node.ts

"use node";

import { v } from "convex/values";
import { internalAction } from "./_generated/server";

export const upload = internalAction({
  args: {
    base64: v.string(),
  },
  handler: async (ctx, args) => {
    const mimeType = args.base64.match(/data:(.*?);base64/)?.[1];
    const base64Data = args.base64.split(",")[1];

    if (!mimeType || !base64Data) return null;

    const buffer = Buffer.from(base64Data, "base64");

    const uploadUrl = await ctx.storage.generateUploadUrl();

    try {
      const result = await fetch(uploadUrl, {
        method: "POST",
        headers: { "Content-Type": mimeType },
        body: buffer,
      });

      const json = await result.json();
      if (!result.ok) return null;

      return json.storageId;
    } catch (e) {
      return null;
    }
  },
});
#

Is this suppose to be slower? faster? better? worse?

hearty lance
#

Yes this is fine! There are some limits: argument size is limited to 8MB. Speed should be similar, the image isn't compressed client-side in either approach.

#

Also base64 isn't the most efficient way to store data, it's got ~33% overhead. Convex functions also support array buffers of binary data, you could do that instead.

hearty lance
#

It's not obvious which would be faster, but if you were sending the image somewhere else instead of sotring it in convex storage then HTTP might be faster because you can do it streaming