96 lines
4.3 KiB
TypeScript
96 lines
4.3 KiB
TypeScript
import { extensionOf, kindOfExtension, kindOfMime } from "./kinds.ts";
|
|
import { defaultRegistry, downloadRenderer } from "./registry.ts";
|
|
import { sniff } from "./sniff.ts";
|
|
import { readBounded, safeSourceUrl } from "./source.ts";
|
|
import type { ResolveOptions, ViewerKind, ViewerPlan, ViewerSource } from "./types.ts";
|
|
|
|
export async function resolve(
|
|
source: ViewerSource,
|
|
options: ResolveOptions = {},
|
|
): Promise<ViewerPlan> {
|
|
const ext = extensionOf(source.name);
|
|
let kind = kindOfMime(source.mime ?? source.blob?.type ?? "") ?? kindOfExtension(ext);
|
|
const fallback = (reason: string): ViewerPlan => ({
|
|
kind,
|
|
renderer: downloadRenderer,
|
|
fallback: [],
|
|
needsConvert: false,
|
|
sandboxed: false,
|
|
reason,
|
|
});
|
|
if ([".exe", ".dll"].includes(ext)) return fallback("Executable files cannot be previewed");
|
|
const registry = options.registry ?? defaultRegistry;
|
|
const candidates = (k: ViewerKind, extension = ext) => registry.candidates(k, extension);
|
|
// No renderer for the kind is not an oversized file: leave the cap open so the
|
|
// ladder reports why nothing can display it rather than "exceeds preview limit".
|
|
const capFor = (k: ViewerKind) => {
|
|
const caps = candidates(k).map((r) => r.maxInlineBytes ?? 2 * 1024 * 1024);
|
|
return caps.length ? Math.max(...caps) : Infinity;
|
|
};
|
|
try {
|
|
let size = source.blob?.size ?? source.size;
|
|
let bytes: Uint8Array;
|
|
if (source.blob) {
|
|
bytes = new Uint8Array(await source.blob.slice(0, 4096).arrayBuffer());
|
|
} else {
|
|
const url = source.src && safeSourceUrl(source.src);
|
|
if (!url) return fallback("No readable file source");
|
|
const request = options.fetch ?? fetch;
|
|
const head = await request(url, { method: "HEAD", signal: options.signal });
|
|
if (head.ok) {
|
|
const length = head.headers.get("content-length");
|
|
if (length !== null && /^\d+$/.test(length)) size = Math.max(size ?? 0, Number(length));
|
|
kind = kindOfMime(head.headers.get("content-type") ?? "") ?? kind;
|
|
}
|
|
if (size !== undefined && size > capFor(kind)) return fallback("File exceeds preview limit");
|
|
const response = await request(url, {
|
|
headers: { Range: "bytes=0-4095" },
|
|
signal: options.signal,
|
|
});
|
|
bytes = await readBounded(response, 4096, true);
|
|
}
|
|
const detected = sniff(bytes);
|
|
if (detected.executable) return fallback("Executable files cannot be previewed");
|
|
const previous = kind;
|
|
// Plain UTF-8 retains code classification; active markup and signatures win.
|
|
// RTF is the one office format that is genuinely text, so `doc` survives a
|
|
// text signature too — otherwise a .rtf loses its extension and its reader.
|
|
// A generic container signature (OLE, ZIP, unrecognised binary) says only
|
|
// "not text", so a .docx or .psd keeps the kind its extension implies.
|
|
const generic = detected.kind === "binary" || detected.kind === "archive";
|
|
const keepsExtensionKind =
|
|
(detected.kind === "text" && ["text", "code", "markup", "sheet", "doc"].includes(kind)) ||
|
|
(generic && ["doc", "sheet", "archive", "binary"].includes(kind));
|
|
kind = keepsExtensionKind ? kind : detected.kind;
|
|
// An extension override must not bypass conflicting signatures.
|
|
const selectedExt =
|
|
kind === previous || (detected.kind === "text" && previous === "binary") ? ext : "";
|
|
const available = [];
|
|
let needsConvert = false;
|
|
for (const renderer of candidates(kind, selectedExt)) {
|
|
// A tier 3 renderer needs a converter, and `load` is how it proves one is
|
|
// reachable. Without that check it could never report itself unavailable.
|
|
if (renderer.tier === 3 && !renderer.load) continue;
|
|
if (size !== undefined && size > (renderer.maxInlineBytes ?? 2 * 1024 * 1024)) continue;
|
|
try {
|
|
await renderer.load?.(options);
|
|
if (renderer.tier === 3) needsConvert = true;
|
|
available.push(renderer);
|
|
} catch {
|
|
/* Optional dependency unavailable. */
|
|
}
|
|
}
|
|
const renderer = available.shift();
|
|
if (!renderer) return fallback("Preview unavailable for this file");
|
|
return {
|
|
kind,
|
|
renderer,
|
|
fallback: [...available, downloadRenderer],
|
|
needsConvert: needsConvert && renderer.tier === 3,
|
|
sandboxed: kind === "markup" || kind === "vector",
|
|
};
|
|
} catch {
|
|
return fallback("File could not be loaded");
|
|
}
|
|
}
|