Saving the files

Next, we have to actually put the files somewhere. In production I would use Vercel Blob but I don’t want to set that up right now, so I’m just going to create a wrapper around the API that we can just locally. Create lib/server/files.ts:

import path from 'node:path';
import fs from 'node:fs';

const base = `node_modules/.squirrel`;

export async function put(name: string, file: File) {
	const filename = path.join(base, name);

	try {
		fs.mkdirSync(path.dirname(filename), { recursive: true });
	} catch {
		// ignore
	}

	fs.writeFileSync(filename, new Uint8Array(await file.arrayBuffer()));
}

// TODO
export async function get(filename: string, file: File) {}
export async function del(filename: string, file: File) {}

When we get ready to deploy to production, we can swap out this API with something suitable for the deployment platform.

We can now use this API inside our uploadFiles form. Create an uploadFile function…

async function uploadFile(data: { file: File; parent: string }) {
	const user = await requireUser();

	let name = data.file.name;
	let i = 0;

	while (true) {
		const [inserted] = await db
			.insert(file)
			.values({
				user_id: user.id,
				name,
				parent: data.parent,
				size: data.file.size,
				type: data.file.type,
				status: 'pending'
			})
			.onConflictDoNothing()
			.returning();

		if (inserted) {
			await put(`${encodeURIComponent(user.id)}/${inserted.id}`, data.file);

			await db.update(file).set({ status: 'ready' }).where(eq(file.id, inserted.id));

			return;
		}

		name = `${data.file.name} (${++i})`;
	}
}

…and use it inside uploadFiles:

export const uploadFiles = form(
	v.object({
		files: v.pipe(v.array(v.file()), v.minLength(1))
	}),
	async (data) => {
		for (const file of data.files) {
			await uploadFile({ file, parent: '<root>' });
		}
	}
);