/* Copyright 2026 Marimo. All rights reserved. */ import { FileIcon, LoaderCircle, RefreshCwIcon } from "lucide-react"; import type React from "react"; import { useCallback } from "react"; import { useLocale } from "react-aria"; import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell"; import { FilePreviewHeader } from "@/components/editor/file-tree/file-header"; import { renderFileIcon } from "@/components/editor/file-tree/file-icons"; import { FileContentRenderer, isMediaMime, } from "@/components/editor/file-tree/renderers"; import { Tooltip } from "@/components/ui/tooltip"; import { toast } from "@/components/ui/use-toast"; import { DownloadStorage } from "@/core/storage/request-registry"; import type { StorageEntry, StorageNamespace } from "@/core/storage/types"; import { useAsyncData } from "@/hooks/useAsyncData"; import { downloadByURL } from "@/utils/download"; import { formatBytes } from "@/utils/formatting"; import { Logger } from "@/utils/Logger"; import { CopyClipboardIcon } from "../icons/copy-icon"; import { Button } from "../ui/button"; import { STORAGE_SNIPPETS } from "./storage-snippets"; const MAX_MEDIA_PREVIEW_SIZE = 100 * 1024 * 1024; // 100 MB interface Props { entry: StorageEntry; namespace: string; protocol: string; backendType: StorageNamespace["backendType"]; onBack: () => void; } function displayName(path: string): string { const trimmed = path.endsWith("/") ? path.slice(0, -1) : path; const parts = trimmed.split("/"); return parts[parts.length - 1] || trimmed; } type PreviewData = | { type: "media"; url: string } | { type: "text"; content: string }; export const StorageFileViewer: React.FC = ({ entry, namespace, protocol, backendType, onBack, }) => { const { locale } = useLocale(); const addCodeToNewCell = useAddCodeToNewCell(); const name = displayName(entry.path); const mime = entry.mimeType || "text/plain"; const isMedia = isMediaMime(mime); const tooLargeForMedia = isMedia && entry.size > MAX_MEDIA_PREVIEW_SIZE && entry.size > 0; const { data: preview, isPending, error, refetch, } = useAsyncData(async (): Promise => { if (tooLargeForMedia) { return null; } const result = await DownloadStorage.request({ namespace, path: entry.path, preview: !isMedia, }); if (result.error) { throw new Error(result.error); } if (!result.url) { throw new Error("No URL returned"); } if (isMedia) { return { type: "media", url: result.url }; } const resp = await fetch(result.url); if (!resp.ok) { throw new Error(`Failed to fetch preview: ${resp.statusText}`); } const content = await resp.text(); return { type: "text", content }; }, [namespace, entry.path, isMedia, tooLargeForMedia]); const handleDownload = useCallback(async () => { try { const result = await DownloadStorage.request({ namespace, path: entry.path, }); if (result.error) { toast({ title: "Download failed", description: result.error, variant: "danger", }); return; } if (result.url) { downloadByURL(result.url, result.filename ?? name); } } catch (error_) { Logger.error("Failed to download storage entry", error_); toast({ title: "Download failed", description: String(error_), variant: "danger", }); } }, [namespace, entry.path, name]); const snippetActions = STORAGE_SNIPPETS.map((snippet) => { const code = snippet.getCode({ variableName: namespace, protocol, entry, backendType, }); if (code === null) { return null; } const Icon = snippet.icon; return ( ); }); const header = ( ); const renderMetadata = ({ includeMime = false, }: { includeMime?: boolean; }) => { return (
Path
{entry.path}
{includeMime && ( Type )} {includeMime && {mime}} {entry.size > 0 && ( <> Size {formatBytes(entry.size, locale)} )} {entry.lastModified != null && ( <> Modified {new Date(entry.lastModified * 1000).toLocaleString()} )}
); }; if (tooLargeForMedia) { return (
{header} {renderMetadata({ includeMime: true })}
File is too large to preview ({formatBytes(entry.size, locale)}).
); } if (isPending) { return (
{header} {renderMetadata({})}
Loading preview...
); } if (error) { return (
{header} {renderMetadata({ includeMime: true })}
Failed to load preview: {error.message}
); } if (preview) { return (
{header} {renderMetadata({})}
); } return (
{header} {renderMetadata({ includeMime: true })}
Preview not available for this file type.
); };