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.