mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-01 10:02:57 +08:00
- Never write preview-only or binary files: handleSave is a no-op when previewKind/isBinary, and the text buffer is cleared when switching to a media/binary file, so Cmd/Ctrl+S can no longer post a stale or empty buffer over the file on disk (CodeRabbit: Data Integrity, Critical). - Choose the fallback MIME type per file extension (single source of truth in previewableFile.ts) instead of per preview-kind, and only override when the server Content-Type is missing or generic, so webm/ogv/ogg/flac/svg render correctly (CodeRabbit: Functional Correctness, Major). - Harden the PDF iframe: validate the %PDF- magic bytes and pin the blob MIME to application/pdf so a mislabeled HTML/SVG file can't execute in the app origin. A sandbox attribute is intentionally not used because Chromium's built-in PDF viewer will not load inside any sandboxed frame (CodeRabbit: Security, Critical). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { api } from '../../../utils/api';
|
|
import type { CodeEditorFile } from '../types/types';
|
|
import { isBinaryFile } from '../utils/binaryFile';
|
|
import { getPreviewKind } from '../utils/previewableFile';
|
|
|
|
type UseCodeEditorDocumentParams = {
|
|
file: CodeEditorFile;
|
|
projectPath?: string;
|
|
};
|
|
|
|
const getErrorMessage = (error: unknown) => {
|
|
if (error instanceof Error) {
|
|
return error.message;
|
|
}
|
|
|
|
return String(error);
|
|
};
|
|
|
|
export const useCodeEditorDocument = ({ file, projectPath }: UseCodeEditorDocumentParams) => {
|
|
const [content, setContent] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
const [isBinary, setIsBinary] = useState(false);
|
|
// Some binaries (images, PDFs, audio, video) can be rendered natively, so the
|
|
// editor shows an inline preview instead of the generic binary placeholder.
|
|
const previewKind = getPreviewKind(file.name);
|
|
// `fileProjectId` is the DB primary key passed down from the editor sidebar;
|
|
// the fallback to `projectPath` preserves older callers that didn't yet
|
|
// propagate the identifier.
|
|
const fileProjectId = file.projectId ?? projectPath;
|
|
const filePath = file.path;
|
|
const fileName = file.name;
|
|
const fileDiffNewString = file.diffInfo?.new_string;
|
|
const fileDiffOldString = file.diffInfo?.old_string;
|
|
|
|
useEffect(() => {
|
|
const loadFileContent = async () => {
|
|
try {
|
|
setLoading(true);
|
|
setIsBinary(false);
|
|
|
|
// Natively previewable media (image/pdf/audio/video) is rendered by
|
|
// CodeEditorMediaPreview, so there is nothing to read as text here.
|
|
// Clear any buffer left over from a previously opened text file so a
|
|
// stray save can't write stale content over the binary file.
|
|
if (getPreviewKind(file.name)) {
|
|
setContent('');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Check if file is binary by extension
|
|
if (isBinaryFile(file.name)) {
|
|
setContent('');
|
|
setIsBinary(true);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Diff payload may already include full old/new snapshots, so avoid disk read.
|
|
if (file.diffInfo && fileDiffNewString !== undefined && fileDiffOldString !== undefined) {
|
|
setContent(fileDiffNewString);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
if (!fileProjectId) {
|
|
throw new Error('Missing project identifier');
|
|
}
|
|
|
|
const response = await api.readFile(fileProjectId, filePath);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load file: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setContent(data.content);
|
|
} catch (error) {
|
|
const message = getErrorMessage(error);
|
|
console.error('Error loading file:', error);
|
|
setContent(`// Error loading file: ${message}\n// File: ${fileName}\n// Path: ${filePath}`);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadFileContent();
|
|
}, [file.diffInfo, file.name, fileDiffNewString, fileDiffOldString, fileName, filePath, fileProjectId]);
|
|
|
|
const handleSave = useCallback(async () => {
|
|
// Preview-only and binary files have no editable text buffer; never write
|
|
// them back (e.g. via Cmd/Ctrl+S) or we'd corrupt the file on disk.
|
|
if (previewKind || isBinaryFile(fileName)) {
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
setSaveError(null);
|
|
|
|
try {
|
|
if (!fileProjectId) {
|
|
throw new Error('Missing project identifier');
|
|
}
|
|
|
|
const response = await api.saveFile(fileProjectId, filePath, content);
|
|
|
|
if (!response.ok) {
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType?.includes('application/json')) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Save failed: ${response.status}`);
|
|
}
|
|
|
|
const textError = await response.text();
|
|
console.error('Non-JSON error response:', textError);
|
|
throw new Error(`Save failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
await response.json();
|
|
|
|
setSaveSuccess(true);
|
|
setTimeout(() => setSaveSuccess(false), 2000);
|
|
} catch (error) {
|
|
const message = getErrorMessage(error);
|
|
console.error('Error saving file:', error);
|
|
setSaveError(message);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [content, filePath, fileProjectId, previewKind, fileName]);
|
|
|
|
const handleDownload = useCallback(() => {
|
|
const blob = new Blob([content], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement('a');
|
|
|
|
anchor.href = url;
|
|
anchor.download = file.name;
|
|
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
document.body.removeChild(anchor);
|
|
|
|
URL.revokeObjectURL(url);
|
|
}, [content, file.name]);
|
|
|
|
return {
|
|
content,
|
|
setContent,
|
|
loading,
|
|
saving,
|
|
saveSuccess,
|
|
saveError,
|
|
isBinary,
|
|
previewKind,
|
|
fileProjectId,
|
|
handleSave,
|
|
handleDownload,
|
|
};
|
|
};
|