diff --git a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx index 0033f077..e0eedfbd 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx @@ -1,5 +1,5 @@ import { useMemo, useRef } from 'react'; -import { Loader2, FolderArchive } from 'lucide-react'; +import { Loader2, FolderArchive, FileWarning, Download } from 'lucide-react'; import { getLang, getRawUrl, getArchiveBaseName } from './file-types'; import { useFileViewer } from './FileViewerContext'; import { PdfRenderer } from './renderers/PdfRenderer'; @@ -14,7 +14,7 @@ import { EditorRenderer } from './renderers/EditorRenderer'; import { JsonEditorRenderer } from './renderers/JsonEditorRenderer'; export const FileViewerBody = () => { - const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent } = + const { filePath, fileName, root, fileType, content, loading, error, autoPlay, editing, setContent, handleDownload } = useFileViewer(); const scrollRef = useRef(null); @@ -38,8 +38,19 @@ export const FileViewerBody = () => { ) : error ? ( -
+ // Being told a file is too big to open, with nothing to do about it, is a dead end. The bytes are + // still there and `/raw` has no size limit — only the UTF-8 read does — so downloading is the + // action that still works, and it is offered rather than left to be guessed at. +
+ {error} +
) : fileType === 'pdf' ? ( diff --git a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerProvider.tsx b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerProvider.tsx index 3560dc2d..b486ee08 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerProvider.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerProvider.tsx @@ -17,6 +17,27 @@ type FileViewerProviderProps = { children: ReactNode; }; +/** + * The server's own sentence, when it is fit to show. + * + * `hono.ts` renders a CustomError as `ctx.text(error.message)`, so "File too large to read (max 5 MB)" + * and "Cannot read a directory" arrive verbatim — and every one of them used to be thrown away for a flat + * "Failed to read file", which tells the reader nothing and sends them looking for a bug that is not + * there. + * + * Guarded rather than trusted: a body is only shown when it looks like prose. An HTML error page from a + * proxy, or a JSON envelope from a route that sets `returnValue`, would otherwise be rendered raw at the + * user — worse than the generic line it replaced. + */ +function readableError(err: unknown): string { + const generic = 'Failed to read file'; + if (!(err instanceof Error) || !err.message) return generic; + const message = err.message.trim(); + if (!message || message.length > 200) return generic; + if (message.startsWith('<') || message.startsWith('{') || message.startsWith('[')) return generic; + return message; +} + export const FileViewerProvider = ({ filePath, fileName, @@ -49,7 +70,13 @@ export const FileViewerProvider = ({ return; } - if (fileType === 'audio' || fileType === 'video' || fileType === 'image' || fileType === 'pdf' || fileType === 'archive') { + if ( + fileType === 'audio' || + fileType === 'video' || + fileType === 'image' || + fileType === 'pdf' || + fileType === 'archive' + ) { setLoading(false); return; } @@ -59,7 +86,7 @@ export const FileViewerProvider = ({ files .readFile(filePath) .then((res) => setContent(res.content)) - .catch(() => setError('Failed to read file')) + .catch((err) => setError(readableError(err))) .finally(() => setLoading(false)); }, [filePath, directContent]);