fix(app): binary file handling in file view (#11312)
This commit is contained in:
@@ -432,6 +432,7 @@ export const dict = {
|
||||
"session.review.noChanges": "لا توجد تغييرات",
|
||||
"session.files.selectToOpen": "اختر ملفًا لفتحه",
|
||||
"session.files.all": "كل الملفات",
|
||||
"session.files.binaryContent": "ملف ثنائي (لا يمكن عرض المحتوى)",
|
||||
"session.messages.renderEarlier": "عرض الرسائل السابقة",
|
||||
"session.messages.loadingEarlier": "جارٍ تحميل الرسائل السابقة...",
|
||||
"session.messages.loadEarlier": "تحميل الرسائل السابقة",
|
||||
|
||||
@@ -433,6 +433,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Sem alterações",
|
||||
"session.files.selectToOpen": "Selecione um arquivo para abrir",
|
||||
"session.files.all": "Todos os arquivos",
|
||||
"session.files.binaryContent": "Arquivo binário (conteúdo não pode ser exibido)",
|
||||
"session.messages.renderEarlier": "Renderizar mensagens anteriores",
|
||||
"session.messages.loadingEarlier": "Carregando mensagens anteriores...",
|
||||
"session.messages.loadEarlier": "Carregar mensagens anteriores",
|
||||
|
||||
@@ -434,6 +434,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Ingen ændringer",
|
||||
"session.files.selectToOpen": "Vælg en fil at åbne",
|
||||
"session.files.all": "Alle filer",
|
||||
"session.files.binaryContent": "Binær fil (indhold kan ikke vises)",
|
||||
"session.messages.renderEarlier": "Vis tidligere beskeder",
|
||||
"session.messages.loadingEarlier": "Indlæser tidligere beskeder...",
|
||||
"session.messages.loadEarlier": "Indlæs tidligere beskeder",
|
||||
|
||||
@@ -442,6 +442,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Keine Änderungen",
|
||||
"session.files.selectToOpen": "Datei zum Öffnen auswählen",
|
||||
"session.files.all": "Alle Dateien",
|
||||
"session.files.binaryContent": "Binärdatei (Inhalt kann nicht angezeigt werden)",
|
||||
"session.messages.renderEarlier": "Frühere Nachrichten rendern",
|
||||
"session.messages.loadingEarlier": "Lade frühere Nachrichten...",
|
||||
"session.messages.loadEarlier": "Frühere Nachrichten laden",
|
||||
|
||||
@@ -441,6 +441,7 @@ export const dict = {
|
||||
|
||||
"session.files.selectToOpen": "Select a file to open",
|
||||
"session.files.all": "All files",
|
||||
"session.files.binaryContent": "Binary file (content cannot be displayed)",
|
||||
|
||||
"session.messages.renderEarlier": "Render earlier messages",
|
||||
"session.messages.loadingEarlier": "Loading earlier messages...",
|
||||
|
||||
@@ -436,6 +436,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Sin cambios",
|
||||
"session.files.selectToOpen": "Selecciona un archivo para abrir",
|
||||
"session.files.all": "Todos los archivos",
|
||||
"session.files.binaryContent": "Archivo binario (el contenido no puede ser mostrado)",
|
||||
"session.messages.renderEarlier": "Renderizar mensajes anteriores",
|
||||
"session.messages.loadingEarlier": "Cargando mensajes anteriores...",
|
||||
"session.messages.loadEarlier": "Cargar mensajes anteriores",
|
||||
|
||||
@@ -441,6 +441,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Aucune modification",
|
||||
"session.files.selectToOpen": "Sélectionnez un fichier à ouvrir",
|
||||
"session.files.all": "Tous les fichiers",
|
||||
"session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)",
|
||||
"session.messages.renderEarlier": "Afficher les messages précédents",
|
||||
"session.messages.loadingEarlier": "Chargement des messages précédents...",
|
||||
"session.messages.loadEarlier": "Charger les messages précédents",
|
||||
|
||||
@@ -433,6 +433,7 @@ export const dict = {
|
||||
"session.review.noChanges": "変更なし",
|
||||
"session.files.selectToOpen": "開くファイルを選択",
|
||||
"session.files.all": "すべてのファイル",
|
||||
"session.files.binaryContent": "バイナリファイル(内容を表示できません)",
|
||||
"session.messages.renderEarlier": "以前のメッセージを表示",
|
||||
"session.messages.loadingEarlier": "以前のメッセージを読み込み中...",
|
||||
"session.messages.loadEarlier": "以前のメッセージを読み込む",
|
||||
|
||||
@@ -435,6 +435,7 @@ export const dict = {
|
||||
"session.review.noChanges": "변경 없음",
|
||||
"session.files.selectToOpen": "열 파일을 선택하세요",
|
||||
"session.files.all": "모든 파일",
|
||||
"session.files.binaryContent": "바이너리 파일 (내용을 표시할 수 없음)",
|
||||
"session.messages.renderEarlier": "이전 메시지 렌더링",
|
||||
"session.messages.loadingEarlier": "이전 메시지 로드 중...",
|
||||
"session.messages.loadEarlier": "이전 메시지 로드",
|
||||
|
||||
@@ -436,6 +436,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Ingen endringer",
|
||||
"session.files.selectToOpen": "Velg en fil å åpne",
|
||||
"session.files.all": "Alle filer",
|
||||
"session.files.binaryContent": "Binær fil (innhold kan ikke vises)",
|
||||
"session.messages.renderEarlier": "Vis tidligere meldinger",
|
||||
"session.messages.loadingEarlier": "Laster inn tidligere meldinger...",
|
||||
"session.messages.loadEarlier": "Last inn tidligere meldinger",
|
||||
|
||||
@@ -435,6 +435,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Brak zmian",
|
||||
"session.files.selectToOpen": "Wybierz plik do otwarcia",
|
||||
"session.files.all": "Wszystkie pliki",
|
||||
"session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)",
|
||||
"session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości",
|
||||
"session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...",
|
||||
"session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości",
|
||||
|
||||
@@ -437,6 +437,7 @@ export const dict = {
|
||||
"session.review.noChanges": "Нет изменений",
|
||||
"session.files.selectToOpen": "Выберите файл, чтобы открыть",
|
||||
"session.files.all": "Все файлы",
|
||||
"session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)",
|
||||
"session.messages.renderEarlier": "Показать предыдущие сообщения",
|
||||
"session.messages.loadingEarlier": "Загрузка предыдущих сообщений...",
|
||||
"session.messages.loadEarlier": "Загрузить предыдущие сообщения",
|
||||
|
||||
@@ -438,6 +438,7 @@ export const dict = {
|
||||
|
||||
"session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด",
|
||||
"session.files.all": "ไฟล์ทั้งหมด",
|
||||
"session.files.binaryContent": "ไฟล์ไบนารี (ไม่สามารถแสดงเนื้อหาได้)",
|
||||
|
||||
"session.messages.renderEarlier": "แสดงข้อความก่อนหน้า",
|
||||
"session.messages.loadingEarlier": "กำลังโหลดข้อความก่อนหน้า...",
|
||||
|
||||
@@ -434,6 +434,7 @@ export const dict = {
|
||||
"session.review.noChanges": "无更改",
|
||||
"session.files.selectToOpen": "选择要打开的文件",
|
||||
"session.files.all": "所有文件",
|
||||
"session.files.binaryContent": "二进制文件(无法显示内容)",
|
||||
"session.messages.renderEarlier": "显示更早的消息",
|
||||
"session.messages.loadingEarlier": "正在加载更早的消息...",
|
||||
"session.messages.loadEarlier": "加载更早的消息",
|
||||
|
||||
@@ -431,6 +431,7 @@ export const dict = {
|
||||
"session.review.noChanges": "沒有變更",
|
||||
"session.files.selectToOpen": "選取要開啟的檔案",
|
||||
"session.files.all": "所有檔案",
|
||||
"session.files.binaryContent": "二進位檔案(無法顯示內容)",
|
||||
"session.messages.renderEarlier": "顯示更早的訊息",
|
||||
"session.messages.loadingEarlier": "正在載入更早的訊息...",
|
||||
"session.messages.loadEarlier": "載入更早的訊息",
|
||||
|
||||
@@ -2342,6 +2342,7 @@ export default function Page() {
|
||||
const c = state()?.content
|
||||
return c?.mimeType === "image/svg+xml"
|
||||
})
|
||||
const isBinary = createMemo(() => state()?.content?.type === "binary")
|
||||
const svgContent = createMemo(() => {
|
||||
if (!isSvg()) return
|
||||
const c = state()?.content
|
||||
@@ -2794,6 +2795,19 @@ export default function Page() {
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded && isBinary()}>
|
||||
<div class="h-full px-6 pb-42 flex flex-col items-center justify-center text-center gap-6">
|
||||
<Mark class="w-14 opacity-10" />
|
||||
<div class="flex flex-col gap-2 max-w-md">
|
||||
<div class="text-14-semibold text-text-strong truncate">
|
||||
{path()?.split("/").pop()}
|
||||
</div>
|
||||
<div class="text-14-regular text-text-weak">
|
||||
{language.t("session.files.binaryContent")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={state()?.loaded}>{renderCode(contents(), "pb-40")}</Match>
|
||||
<Match when={state()?.loading}>
|
||||
<div class="px-6 py-4 text-text-weak">{language.t("common.loading")}...</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ export namespace File {
|
||||
|
||||
export const Content = z
|
||||
.object({
|
||||
type: z.literal("text"),
|
||||
type: z.enum(["text", "binary"]),
|
||||
content: z.string(),
|
||||
diff: z.string().optional(),
|
||||
patch: z
|
||||
@@ -73,6 +73,174 @@ export namespace File {
|
||||
})
|
||||
export type Content = z.infer<typeof Content>
|
||||
|
||||
const binaryExtensions = new Set([
|
||||
"exe",
|
||||
"dll",
|
||||
"pdb",
|
||||
"bin",
|
||||
"so",
|
||||
"dylib",
|
||||
"o",
|
||||
"a",
|
||||
"lib",
|
||||
"wav",
|
||||
"mp3",
|
||||
"ogg",
|
||||
"oga",
|
||||
"ogv",
|
||||
"ogx",
|
||||
"flac",
|
||||
"aac",
|
||||
"wma",
|
||||
"m4a",
|
||||
"weba",
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"webm",
|
||||
"mkv",
|
||||
"zip",
|
||||
"tar",
|
||||
"gz",
|
||||
"gzip",
|
||||
"bz",
|
||||
"bz2",
|
||||
"bzip",
|
||||
"bzip2",
|
||||
"7z",
|
||||
"rar",
|
||||
"xz",
|
||||
"lz",
|
||||
"z",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"dmg",
|
||||
"iso",
|
||||
"img",
|
||||
"vmdk",
|
||||
"ttf",
|
||||
"otf",
|
||||
"woff",
|
||||
"woff2",
|
||||
"eot",
|
||||
"sqlite",
|
||||
"db",
|
||||
"mdb",
|
||||
"apk",
|
||||
"ipa",
|
||||
"aab",
|
||||
"xapk",
|
||||
"app",
|
||||
"pkg",
|
||||
"deb",
|
||||
"rpm",
|
||||
"snap",
|
||||
"flatpak",
|
||||
"appimage",
|
||||
"msi",
|
||||
"msp",
|
||||
"jar",
|
||||
"war",
|
||||
"ear",
|
||||
"class",
|
||||
"kotlin_module",
|
||||
"dex",
|
||||
"vdex",
|
||||
"odex",
|
||||
"oat",
|
||||
"art",
|
||||
"wasm",
|
||||
"wat",
|
||||
"bc",
|
||||
"ll",
|
||||
"s",
|
||||
"ko",
|
||||
"sys",
|
||||
"drv",
|
||||
"efi",
|
||||
"rom",
|
||||
"com",
|
||||
"bat",
|
||||
"cmd",
|
||||
"ps1",
|
||||
"sh",
|
||||
"bash",
|
||||
"zsh",
|
||||
"fish",
|
||||
])
|
||||
|
||||
const imageExtensions = new Set([
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"bmp",
|
||||
"webp",
|
||||
"ico",
|
||||
"tif",
|
||||
"tiff",
|
||||
"svg",
|
||||
"svgz",
|
||||
"avif",
|
||||
"apng",
|
||||
"jxl",
|
||||
"heic",
|
||||
"heif",
|
||||
"raw",
|
||||
"cr2",
|
||||
"nef",
|
||||
"arw",
|
||||
"dng",
|
||||
"orf",
|
||||
"raf",
|
||||
"pef",
|
||||
"x3f",
|
||||
])
|
||||
|
||||
function isImageByExtension(filepath: string): boolean {
|
||||
const ext = path.extname(filepath).toLowerCase().slice(1)
|
||||
return imageExtensions.has(ext)
|
||||
}
|
||||
|
||||
function getImageMimeType(filepath: string): string {
|
||||
const ext = path.extname(filepath).toLowerCase().slice(1)
|
||||
const mimeTypes: Record<string, string> = {
|
||||
png: "image/png",
|
||||
jpg: "image/jpeg",
|
||||
jpeg: "image/jpeg",
|
||||
gif: "image/gif",
|
||||
bmp: "image/bmp",
|
||||
webp: "image/webp",
|
||||
ico: "image/x-icon",
|
||||
tif: "image/tiff",
|
||||
tiff: "image/tiff",
|
||||
svg: "image/svg+xml",
|
||||
svgz: "image/svg+xml",
|
||||
avif: "image/avif",
|
||||
apng: "image/apng",
|
||||
jxl: "image/jxl",
|
||||
heic: "image/heic",
|
||||
heif: "image/heif",
|
||||
}
|
||||
return mimeTypes[ext] || "image/" + ext
|
||||
}
|
||||
|
||||
function isBinaryByExtension(filepath: string): boolean {
|
||||
const ext = path.extname(filepath).toLowerCase().slice(1)
|
||||
return binaryExtensions.has(ext)
|
||||
}
|
||||
|
||||
function isImage(mimeType: string): boolean {
|
||||
return mimeType.startsWith("image/")
|
||||
}
|
||||
|
||||
async function shouldEncode(file: BunFile): Promise<boolean> {
|
||||
const type = file.type?.toLowerCase()
|
||||
log.info("shouldEncode", { type })
|
||||
@@ -83,30 +251,10 @@ export namespace File {
|
||||
|
||||
const parts = type.split("/", 2)
|
||||
const top = parts[0]
|
||||
const rest = parts[1] ?? ""
|
||||
const sub = rest.split(";", 1)[0]
|
||||
|
||||
const tops = ["image", "audio", "video", "font", "model", "multipart"]
|
||||
if (tops.includes(top)) return true
|
||||
|
||||
const bins = [
|
||||
"zip",
|
||||
"gzip",
|
||||
"bzip",
|
||||
"compressed",
|
||||
"binary",
|
||||
"pdf",
|
||||
"msword",
|
||||
"powerpoint",
|
||||
"excel",
|
||||
"ogg",
|
||||
"exe",
|
||||
"dmg",
|
||||
"iso",
|
||||
"rar",
|
||||
]
|
||||
if (bins.some((mark) => sub.includes(mark))) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -287,6 +435,22 @@ export namespace File {
|
||||
throw new Error(`Access denied: path escapes project directory`)
|
||||
}
|
||||
|
||||
// Fast path: check extension before any filesystem operations
|
||||
if (isImageByExtension(file)) {
|
||||
const bunFile = Bun.file(full)
|
||||
if (await bunFile.exists()) {
|
||||
const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0))
|
||||
const content = Buffer.from(buffer).toString("base64")
|
||||
const mimeType = getImageMimeType(file)
|
||||
return { type: "text", content, mimeType, encoding: "base64" }
|
||||
}
|
||||
return { type: "text", content: "" }
|
||||
}
|
||||
|
||||
if (isBinaryByExtension(file)) {
|
||||
return { type: "binary", content: "" }
|
||||
}
|
||||
|
||||
const bunFile = Bun.file(full)
|
||||
|
||||
if (!(await bunFile.exists())) {
|
||||
@@ -294,11 +458,15 @@ export namespace File {
|
||||
}
|
||||
|
||||
const encode = await shouldEncode(bunFile)
|
||||
const mimeType = bunFile.type || "application/octet-stream"
|
||||
|
||||
if (encode && !isImage(mimeType)) {
|
||||
return { type: "binary", content: "", mimeType }
|
||||
}
|
||||
|
||||
if (encode) {
|
||||
const buffer = await bunFile.arrayBuffer().catch(() => new ArrayBuffer(0))
|
||||
const content = Buffer.from(buffer).toString("base64")
|
||||
const mimeType = bunFile.type || "application/octet-stream"
|
||||
return { type: "text", content, mimeType, encoding: "base64" }
|
||||
}
|
||||
|
||||
|
||||
@@ -1554,7 +1554,7 @@ export type FileNode = {
|
||||
}
|
||||
|
||||
export type FileContent = {
|
||||
type: "text"
|
||||
type: "text" | "binary"
|
||||
content: string
|
||||
diff?: string
|
||||
patch?: {
|
||||
|
||||
@@ -2042,7 +2042,7 @@ export type FileNode = {
|
||||
}
|
||||
|
||||
export type FileContent = {
|
||||
type: "text"
|
||||
type: "text" | "binary"
|
||||
content: string
|
||||
diff?: string
|
||||
patch?: {
|
||||
|
||||
Reference in New Issue
Block a user