Folders
So far we’ve only uploaded files, because that’s all you can do with <input type="file">. But we want to be able to upload folders as well.
For that, we need to go beyond <form>, and use JavaScript to implement drag and drop.
First, add a dropzone with event handlers:
<div class="files">
{@render list(await getFiles('<root>'))}
+ <div
+ class="dropzone"
+ role="region"
+ aria-label="File upload drop zone"
+ ondragover={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (e.dataTransfer && e.dataTransfer.items.length > 0) {
+ e.dataTransfer.dropEffect = 'copy';
+ }
+ }}
+ ondrop={async (e) => {
+ e.preventDefault();
+ }}
+ >
+ <span>Drop files here to upload</span>
+ </div>
</div> Update the CSS:
.files {
display: flex;
flex-direction: column;
height: 100vh;
overflow-y: auto;
padding: 0 0 6rem 0;
.dropzone {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-height: 10rem;
}
} The user might drop a folder, so we’re going to need a new remote function. This time, we’re going to use command, which is similar to form in that it runs logic on the server, but you use it like a regular function:
export const createFolder = command(
v.object({
name: v.string(),
parent: v.string()
}),
async (data) => {
const user = await requireUser();
let name = data.name;
let i = 0;
while (true) {
const [inserted] = await db
.insert(file)
.values({
user_id: user.id,
name,
parent: data.parent,
size: 0,
type: 'application/x-directory',
status: 'ready'
})
.onConflictDoNothing()
.returning();
if (inserted) {
getFiles(data.parent).refresh();
return inserted.id;
}
name = `${data.name} (${++i})`;
}
}
); We can also turn our existing uploadFile function into a command, so that we can call that from the browser as well:
+export const uploadFile = command(
+ v.object({
+ file: v.file(),
+ parent: v.string()
+ }),
async (data) => {
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));
+ getFiles(data.parent).refresh();
return;
}
name = `${data.file.name} (${++i})`;
}
}
); Because we can call it from the browser, we need to tell it which queries need to be refreshed — in our case, just getFiles.
Finally we just have to wire it up, using the web’s filesystem APIs, which — fair warning — are horrific. Add this function to the <script> element…
async function handleEntry(entry: FileSystemEntry, parent: string) {
if (entry.isDirectory) {
const id = await createFolder({ name: entry.name, parent });
const reader = (entry as FileSystemDirectoryEntry).createReader();
while (true) {
const entries: FileSystemEntry[] = await new Promise((resolve, reject) => {
reader.readEntries(resolve, reject);
});
if (entries.length === 0) {
break;
}
for (const entry of entries) {
await handleEntry(entry, id);
}
}
}
if (entry.isFile) {
const file: File = await new Promise((resolve, reject) => {
(entry as FileSystemFileEntry).file(resolve, reject);
});
await uploadFile({
file,
parent
});
}
} …then call it from the ondrop handler:
e.preventDefault();
+if (e.dataTransfer?.items.length) {
+ const entries = Array.from(e.dataTransfer.items, (item) => item.webkitGetAsEntry());
+ for (const entry of entries) {
+ if (entry) await handleEntry(entry, '<root>');
+ }
+}