kstulgys
kstulgys5mo ago

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

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" });
};
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" });
};
3 Replies
kstulgys
kstulgysOP5mo ago
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()]);
},
});
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;
}
},
});
"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?
ballingt
ballingt4mo ago
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. 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
kstulgys
kstulgysOP4mo ago
Thank you for the insights!

Did you find this page helpful?