/* Copyright 2026 Marimo. All rights reserved. */
import { type DropzoneOptions, useDropzone } from "react-dropzone";
import { toast } from "@/components/ui/use-toast";
import { useRequestClient } from "@/core/network/requests";
import { serializeBlob } from "@/utils/blob";
import { withLoadingToast } from "@/utils/download";
import { Logger } from "@/utils/Logger";
import { type FilePath, PathBuilder } from "@/utils/paths";
import { refreshRoot } from "./state";
const MAX_SIZE = 1024 * 1024 * 100; // 100MB
export function useFileExplorerUpload(options: DropzoneOptions = {}) {
const { sendCreateFileOrFolder } = useRequestClient();
return useDropzone({
multiple: true,
maxSize: MAX_SIZE,
onError: (error) => {
Logger.error(error);
toast({
title: "File upload failed",
description: error.message,
variant: "danger",
});
},
onDropRejected: (rejectedFiles) => {
toast({
title: "File upload failed",
description: (
{rejectedFiles.map((file) => (
{file.file.name} ({file.errors.map((e) => e.message).join(", ")}
)
))}
),
variant: "danger",
});
},
onDrop: async (acceptedFiles) => {
if (acceptedFiles.length === 0) {
return;
}
const isSingle = acceptedFiles.length === 1;
const loadingTitle = isSingle
? "Uploading file..."
: "Uploading files...";
const onFinish = {
title: isSingle
? "File uploaded"
: `${acceptedFiles.length} files uploaded`,
};
await withLoadingToast(
loadingTitle,
async (progress) => {
progress.addTotal(acceptedFiles.length);
for (const file of acceptedFiles) {
// We strip the leading slash since File.path can return
// `/path/to/file`.
const filePath = stripLeadingSlash(getPath(file));
let directoryPath = "" as FilePath;
if (filePath) {
directoryPath =
PathBuilder.guessDeliminator(filePath).dirname(filePath);
}
// File contents are sent base64-encoded to support arbitrary
// bytes data
//
// get the raw base64-encoded data from a string starting with
// data:*/*;base64,
const base64 = (await serializeBlob(file)).split(",")[1];
await sendCreateFileOrFolder({
path: directoryPath,
type: "file",
name: file.name,
contents: base64,
});
progress.increment(1);
}
await refreshRoot();
},
onFinish,
);
},
...options,
});
}
/**
* Get the path of a file.
*
* Types only have `webkitRelativePath`, but File objects in the browser
* can have `path` and `relativePath`.
*/
function getPath(file: File): FilePath | undefined {
if (file.webkitRelativePath) {
return file.webkitRelativePath as FilePath;
}
if ("path" in file && typeof file.path === "string") {
return file.path as FilePath;
}
if ("relativePath" in file && typeof file.relativePath === "string") {
return file.relativePath as FilePath;
}
return undefined;
}
/**
* Strip leading slashes from a path.
*
* TODO: this may not support windows paths.
*/
function stripLeadingSlash(path: FilePath | undefined): FilePath | undefined {
if (!path) {
return undefined;
}
return path.replace(/^\/+/, "") as FilePath;
}