/* 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; }