Files

We can log in and log out, but we can’t do anything yet. First, we need to create a new table in our database:

export const file = sqliteTable(
	'file',
	{
		id: text('id')
			.notNull()
			.$defaultFn(() => crypto.randomUUID())
			.primaryKey(),
		user_id: text('user_id')
			.notNull()
			.references(() => user.id),
		name: text('name').notNull(),
		parent: text('parent').notNull().default('<root>'),
		size: integer('size').notNull(),
		type: text('type').notNull(),
		status: text({ enum: ['pending', 'ready'] })
			.notNull()
			.default('pending'),
		shared: integer({ mode: 'boolean' }).notNull().default(false),
		created_at: integer('created_at', { mode: 'timestamp' })
			.notNull()
			.$defaultFn(() => new Date()),
		updated_at: integer('updated_at', { mode: 'timestamp' })
			.notNull()
			.$onUpdate(() => new Date())
	},
	(t) => [unique('unique_file').on(t.user_id, t.parent, t.name)]
);

Then we want to create a remote function that lets us get that data. Create a new file, src/lib/files.remote.ts:

import { query } from '$app/server';
import * as v from 'valibot';
import { requireUser } from './auth.remote';
import { db } from './server/db';
import { file } from './server/db/schema';
import { and, eq } from 'drizzle-orm';

export const getFiles = query(v.string(), async (parent) => {
	const user = await requireUser();

	const data = await db
		.select()
		.from(file)
		.where(and(eq(file.user_id, user.id), eq(file.parent, parent), eq(file.status, 'ready')));

	return data;
});

Now we can start building our file viewer. We can create a Files.svelte component right inside src/routes — because it doesn’t start with +, it won’t be treated as a route file:

<script lang="ts">
	import { getFiles } from '$lib/files.remote';
	import { file } from '$lib/server/db/schema';
</script>

<div class="files">
	{@render list(await getFiles('<root>'))}
</div>

{#snippet list(files: (typeof file.$inferSelect)[])}
	<div role="list">
		{#each files as f (f.id)}
			<div role="listitem">
				{f.name}
			</div>
		{/each}
	</div>
{/snippet}

Add it to +page.svelte:

<main>
	<Files />
</main>

It works… but we have no data! We need to create an uploadFiles form and an Uploader.svelte component. Scaffold it out:

export const uploadFiles = form(
	v.object({
		files: v.pipe(v.array(v.file()), v.minLength(1))
	}),
	async (data) => {
		console.log(data);
	}
);

For the Uploader.svelte component, we’ll need a suitable icon. You can find good libraries for things like this on svelte.dev/packages. For now, we’ll install Lucide:

pnpm add @lucide/svelte
<script>
	import UploadIcon from '@lucide/svelte/icons/upload-cloud';
	import { uploadFiles } from '$lib/files.remote';
</script>

<form {...uploadFiles} enctype="multipart/form-data">
	<label>
		<input {...uploadFiles.fields.files.as('file multiple')} required />
		Upload
		<UploadIcon />
	</label>

	<button class="rounded" type="reset">cancel</button>
	<button class="rounded primary" type="submit">
		Upload
		<UploadIcon />
	</button>
</form>

<style>
	form {
		height: 100%;
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 0.5rem;

		&:not(:has(input:valid)) {
			button {
				display: none;
			}
		}

		&:has(input:valid) {
			label {
				display: none;
			}
		}
	}

	label {
		height: 100%;
		display: flex;
		padding: 0.5rem 1rem;
		align-items: center;
		justify-content: center;
		gap: 0.5rem;
		cursor: pointer;

		input {
			position: absolute;
			opacity: 0;
			pointer-events: none;
		}
	}
</style>

Try uploading something.