chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -8,10 +8,7 @@ type FetchMediaResult = {
|
||||
fileName?: string;
|
||||
};
|
||||
|
||||
export type FetchLike = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
) => Promise<Response>;
|
||||
export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
type FetchMediaOptions = {
|
||||
url: string;
|
||||
@@ -23,9 +20,7 @@ function stripQuotes(value: string): string {
|
||||
return value.replace(/^["']|["']$/g, "");
|
||||
}
|
||||
|
||||
function parseContentDispositionFileName(
|
||||
header?: string | null,
|
||||
): string | undefined {
|
||||
function parseContentDispositionFileName(header?: string | null): string | undefined {
|
||||
if (!header) return undefined;
|
||||
const starMatch = /filename\*\s*=\s*([^;]+)/i.exec(header);
|
||||
if (starMatch?.[1]) {
|
||||
@@ -42,10 +37,7 @@ function parseContentDispositionFileName(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function readErrorBodySnippet(
|
||||
res: Response,
|
||||
maxChars = 200,
|
||||
): Promise<string | undefined> {
|
||||
async function readErrorBodySnippet(res: Response, maxChars = 200): Promise<string | undefined> {
|
||||
try {
|
||||
const text = await res.text();
|
||||
if (!text) return undefined;
|
||||
@@ -58,9 +50,7 @@ async function readErrorBodySnippet(
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRemoteMedia(
|
||||
options: FetchMediaOptions,
|
||||
): Promise<FetchMediaResult> {
|
||||
export async function fetchRemoteMedia(options: FetchMediaOptions): Promise<FetchMediaResult> {
|
||||
const { url, fetchImpl, filePathHint } = options;
|
||||
const fetcher: FetchLike | undefined = fetchImpl ?? globalThis.fetch;
|
||||
if (!fetcher) {
|
||||
@@ -76,8 +66,7 @@ export async function fetchRemoteMedia(
|
||||
|
||||
if (!res.ok) {
|
||||
const statusText = res.statusText ? ` ${res.statusText}` : "";
|
||||
const redirected =
|
||||
res.url && res.url !== url ? ` (redirected to ${res.url})` : "";
|
||||
const redirected = res.url && res.url !== url ? ` (redirected to ${res.url})` : "";
|
||||
let detail = `HTTP ${res.status}${statusText}`;
|
||||
if (!res.body) {
|
||||
detail = `HTTP ${res.status}${statusText}; empty response body`;
|
||||
@@ -85,9 +74,7 @@ export async function fetchRemoteMedia(
|
||||
const snippet = await readErrorBodySnippet(res);
|
||||
if (snippet) detail += `; body: ${snippet}`;
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to fetch media from ${url}${redirected}: ${detail}`,
|
||||
);
|
||||
throw new Error(`Failed to fetch media from ${url}${redirected}: ${detail}`);
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await res.arrayBuffer());
|
||||
@@ -100,18 +87,12 @@ export async function fetchRemoteMedia(
|
||||
// ignore parse errors; leave undefined
|
||||
}
|
||||
|
||||
const headerFileName = parseContentDispositionFileName(
|
||||
res.headers.get("content-disposition"),
|
||||
);
|
||||
const headerFileName = parseContentDispositionFileName(res.headers.get("content-disposition"));
|
||||
let fileName =
|
||||
headerFileName ||
|
||||
fileNameFromUrl ||
|
||||
(filePathHint ? path.basename(filePathHint) : undefined);
|
||||
headerFileName || fileNameFromUrl || (filePathHint ? path.basename(filePathHint) : undefined);
|
||||
|
||||
const filePathForMime =
|
||||
headerFileName && path.extname(headerFileName)
|
||||
? headerFileName
|
||||
: (filePathHint ?? url);
|
||||
headerFileName && path.extname(headerFileName) ? headerFileName : (filePathHint ?? url);
|
||||
const contentType = await detectMime({
|
||||
buffer,
|
||||
headerMime: res.headers.get("content-type"),
|
||||
|
||||
@@ -12,10 +12,7 @@ const logInfo = vi.fn();
|
||||
vi.mock("./store.js", () => ({ saveMediaSource }));
|
||||
vi.mock("../infra/tailscale.js", () => ({ getTailnetHostname }));
|
||||
vi.mock("../infra/ports.js", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../infra/ports.js")>(
|
||||
"../infra/ports.js",
|
||||
);
|
||||
const actual = await vi.importActual<typeof import("../infra/ports.js")>("../infra/ports.js");
|
||||
return { ensurePortAvailable, PortInUseError: actual.PortInUseError };
|
||||
});
|
||||
vi.mock("./server.js", () => ({ startMediaServer }));
|
||||
@@ -39,9 +36,9 @@ describe("ensureMediaHosted", () => {
|
||||
ensurePortAvailable.mockResolvedValue(undefined);
|
||||
const rmSpy = vi.spyOn(fs, "rm").mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
ensureMediaHosted("/tmp/file1", { startServer: false }),
|
||||
).rejects.toThrow("requires the webhook/Funnel server");
|
||||
await expect(ensureMediaHosted("/tmp/file1", { startServer: false })).rejects.toThrow(
|
||||
"requires the webhook/Funnel server",
|
||||
);
|
||||
expect(rmSpy).toHaveBeenCalledWith("/tmp/file1");
|
||||
rmSpy.mockRestore();
|
||||
});
|
||||
@@ -61,11 +58,7 @@ describe("ensureMediaHosted", () => {
|
||||
startServer: true,
|
||||
port: 1234,
|
||||
});
|
||||
expect(startMediaServer).toHaveBeenCalledWith(
|
||||
1234,
|
||||
expect.any(Number),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(startMediaServer).toHaveBeenCalledWith(1234, expect.any(Number), expect.anything());
|
||||
expect(logInfo).toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
url: "https://tail.net/media/id2",
|
||||
|
||||
@@ -18,9 +18,7 @@ function isBun(): boolean {
|
||||
function prefersSips(): boolean {
|
||||
return (
|
||||
process.env.CLAWDBOT_IMAGE_BACKEND === "sips" ||
|
||||
(process.env.CLAWDBOT_IMAGE_BACKEND !== "sharp" &&
|
||||
isBun() &&
|
||||
process.platform === "darwin")
|
||||
(process.env.CLAWDBOT_IMAGE_BACKEND !== "sharp" && isBun() && process.platform === "darwin")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,9 +37,7 @@ async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
|
||||
}
|
||||
}
|
||||
|
||||
async function sipsMetadataFromBuffer(
|
||||
buffer: Buffer,
|
||||
): Promise<ImageMetadata | null> {
|
||||
async function sipsMetadataFromBuffer(buffer: Buffer): Promise<ImageMetadata | null> {
|
||||
return await withTempDir(async (dir) => {
|
||||
const input = path.join(dir, "in.img");
|
||||
await fs.writeFile(input, buffer);
|
||||
@@ -94,9 +90,7 @@ async function sipsResizeToJpeg(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getImageMetadata(
|
||||
buffer: Buffer,
|
||||
): Promise<ImageMetadata | null> {
|
||||
export async function getImageMetadata(buffer: Buffer): Promise<ImageMetadata | null> {
|
||||
if (prefersSips()) {
|
||||
return await sipsMetadataFromBuffer(buffer).catch(() => null);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { detectMime, imageMimeFromFormat } from "./mime.js";
|
||||
|
||||
async function makeOoxmlZip(opts: {
|
||||
mainMime: string;
|
||||
partPath: string;
|
||||
}): Promise<Buffer> {
|
||||
async function makeOoxmlZip(opts: { mainMime: string; partPath: string }): Promise<Buffer> {
|
||||
const zip = new JSZip();
|
||||
zip.file(
|
||||
"[Content_Types].xml",
|
||||
@@ -28,26 +25,20 @@ describe("mime detection", () => {
|
||||
|
||||
it("detects docx from buffer", async () => {
|
||||
const buf = await makeOoxmlZip({
|
||||
mainMime:
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
mainMime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
partPath: "/word/document.xml",
|
||||
});
|
||||
const mime = await detectMime({ buffer: buf, filePath: "/tmp/file.bin" });
|
||||
expect(mime).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
);
|
||||
expect(mime).toBe("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
});
|
||||
|
||||
it("detects pptx from buffer", async () => {
|
||||
const buf = await makeOoxmlZip({
|
||||
mainMime:
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
mainMime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
partPath: "/ppt/presentation.xml",
|
||||
});
|
||||
const mime = await detectMime({ buffer: buf, filePath: "/tmp/file.bin" });
|
||||
expect(mime).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
);
|
||||
expect(mime).toBe("application/vnd.openxmlformats-officedocument.presentationml.presentation");
|
||||
});
|
||||
|
||||
it("prefers extension mapping over generic zip", async () => {
|
||||
@@ -59,8 +50,6 @@ describe("mime detection", () => {
|
||||
buffer: buf,
|
||||
filePath: "/tmp/file.xlsx",
|
||||
});
|
||||
expect(mime).toBe(
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
);
|
||||
expect(mime).toBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,11 +22,9 @@ const EXT_BY_MIME: Record<string, string> = {
|
||||
"application/msword": ".doc",
|
||||
"application/vnd.ms-excel": ".xls",
|
||||
"application/vnd.ms-powerpoint": ".ppt",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
||||
".docx",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
||||
".pptx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
||||
"text/csv": ".csv",
|
||||
"text/plain": ".txt",
|
||||
"text/markdown": ".md",
|
||||
@@ -133,9 +131,7 @@ export function isGifMedia(opts: {
|
||||
return ext === ".gif";
|
||||
}
|
||||
|
||||
export function imageMimeFromFormat(
|
||||
format?: string | null,
|
||||
): string | undefined {
|
||||
export function imageMimeFromFormat(format?: string | null): string | undefined {
|
||||
if (!format) return undefined;
|
||||
switch (format.toLowerCase()) {
|
||||
case "jpg":
|
||||
|
||||
@@ -18,18 +18,11 @@ function isValidMedia(candidate: string) {
|
||||
if (!candidate) return false;
|
||||
if (candidate.length > 1024) return false;
|
||||
if (/\s/.test(candidate)) return false;
|
||||
return (
|
||||
/^https?:\/\//i.test(candidate) ||
|
||||
candidate.startsWith("/") ||
|
||||
candidate.startsWith("./")
|
||||
);
|
||||
return /^https?:\/\//i.test(candidate) || candidate.startsWith("/") || candidate.startsWith("./");
|
||||
}
|
||||
|
||||
// Check if a character offset is inside any fenced code block
|
||||
function isInsideFence(
|
||||
fenceSpans: Array<{ start: number; end: number }>,
|
||||
offset: number,
|
||||
): boolean {
|
||||
function isInsideFence(fenceSpans: Array<{ start: number; end: number }>, offset: number): boolean {
|
||||
return fenceSpans.some((span) => offset >= span.start && offset < span.end);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@ describe("media server", () => {
|
||||
const server = await startMediaServer(0, 5_000);
|
||||
const port = (server.address() as AddressInfo).port;
|
||||
// URL-encoded "../" to bypass client-side path normalization
|
||||
const res = await fetch(
|
||||
`http://localhost:${port}/media/%2e%2e%2fpackage.json`,
|
||||
);
|
||||
const res = await fetch(`http://localhost:${port}/media/%2e%2e%2fpackage.json`);
|
||||
expect(res.status).toBe(400);
|
||||
expect(await res.text()).toBe("invalid path");
|
||||
await new Promise((r) => server.close(r));
|
||||
|
||||
@@ -3,15 +3,7 @@ import path from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
import JSZip from "jszip";
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const realOs = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
const HOME = path.join(realOs.tmpdir(), "clawdbot-home-redirect");
|
||||
|
||||
@@ -22,9 +22,7 @@ describe("media store", () => {
|
||||
await withTempStore(async (store, home) => {
|
||||
const dir = await store.ensureMediaDir();
|
||||
expect(isPathWithinBase(home, dir)).toBe(true);
|
||||
expect(path.normalize(dir)).toContain(
|
||||
`${path.sep}.clawdbot${path.sep}media`,
|
||||
);
|
||||
expect(path.normalize(dir)).toContain(`${path.sep}.clawdbot${path.sep}media`);
|
||||
const stat = await fs.stat(dir);
|
||||
expect(stat.isDirectory()).toBe(true);
|
||||
});
|
||||
@@ -49,9 +47,7 @@ describe("media store", () => {
|
||||
expect(savedJpeg.path.endsWith(".jpg")).toBe(true);
|
||||
|
||||
const huge = Buffer.alloc(5 * 1024 * 1024 + 1);
|
||||
await expect(store.saveMediaBuffer(huge)).rejects.toThrow(
|
||||
"Media exceeds 5MB limit",
|
||||
);
|
||||
await expect(store.saveMediaBuffer(huge)).rejects.toThrow("Media exceeds 5MB limit");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -82,14 +82,9 @@ async function downloadToFile(
|
||||
});
|
||||
pipeline(res, out)
|
||||
.then(() => {
|
||||
const sniffBuffer = Buffer.concat(
|
||||
sniffChunks,
|
||||
Math.min(sniffLen, 16384),
|
||||
);
|
||||
const sniffBuffer = Buffer.concat(sniffChunks, Math.min(sniffLen, 16384));
|
||||
const rawHeader = res.headers["content-type"];
|
||||
const headerMime = Array.isArray(rawHeader)
|
||||
? rawHeader[0]
|
||||
: rawHeader;
|
||||
const headerMime = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
||||
resolve({
|
||||
headerMime,
|
||||
sniffBuffer,
|
||||
@@ -121,18 +116,13 @@ export async function saveMediaSource(
|
||||
const baseId = crypto.randomUUID();
|
||||
if (looksLikeUrl(source)) {
|
||||
const tempDest = path.join(dir, `${baseId}.tmp`);
|
||||
const { headerMime, sniffBuffer, size } = await downloadToFile(
|
||||
source,
|
||||
tempDest,
|
||||
headers,
|
||||
);
|
||||
const { headerMime, sniffBuffer, size } = await downloadToFile(source, tempDest, headers);
|
||||
const mime = await detectMime({
|
||||
buffer: sniffBuffer,
|
||||
headerMime,
|
||||
filePath: source,
|
||||
});
|
||||
const ext =
|
||||
extensionForMime(mime) ?? path.extname(new URL(source).pathname);
|
||||
const ext = extensionForMime(mime) ?? path.extname(new URL(source).pathname);
|
||||
const id = ext ? `${baseId}${ext}` : baseId;
|
||||
const finalDest = path.join(dir, id);
|
||||
await fs.rename(tempDest, finalDest);
|
||||
@@ -162,16 +152,12 @@ export async function saveMediaBuffer(
|
||||
maxBytes = MAX_BYTES,
|
||||
): Promise<SavedMedia> {
|
||||
if (buffer.byteLength > maxBytes) {
|
||||
throw new Error(
|
||||
`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`,
|
||||
);
|
||||
throw new Error(`Media exceeds ${(maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
|
||||
}
|
||||
const dir = path.join(MEDIA_DIR, subdir);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
const baseId = crypto.randomUUID();
|
||||
const headerExt = extensionForMime(
|
||||
contentType?.split(";")[0]?.trim() ?? undefined,
|
||||
);
|
||||
const headerExt = extensionForMime(contentType?.split(";")[0]?.trim() ?? undefined);
|
||||
const mime = await detectMime({ buffer, headerMime: contentType });
|
||||
const ext = headerExt ?? extensionForMime(mime);
|
||||
const id = ext ? `${baseId}${ext}` : baseId;
|
||||
|
||||
Reference in New Issue
Block a user