openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
243 lines (242 loc) • 6.68 kB
JavaScript
import { a as root } from "./secure-temp-dir-XAWcZnE2.js";
import "./security-runtime-CQm7DD1u.js";
import { i as statRequiredDirectory, n as readAbsolutePath, r as resolveCanonicalReadPath, t as classifyFsSafeReadError } from "./path-errors-XUg5zBap.js";
import path from "node:path";
import { spawn } from "node:child_process";
import crypto from "node:crypto";
//#region extensions/file-transfer/src/node-host/dir-fetch.ts
const DIR_FETCH_HARD_MAX_BYTES = 16 * 1024 * 1024;
const DIR_FETCH_DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
function clampMaxBytes(input) {
if (typeof input !== "number" || !Number.isFinite(input) || input <= 0) return DIR_FETCH_DEFAULT_MAX_BYTES;
return Math.min(Math.floor(input), DIR_FETCH_HARD_MAX_BYTES);
}
function classifyFsError(err) {
const safeCode = classifyFsSafeReadError(err);
if (safeCode) return safeCode;
if (err?.code === "ENOENT") return "NOT_FOUND";
return "READ_ERROR";
}
async function preflightDu(dirPath, maxBytes) {
const heuristicKb = Math.ceil(maxBytes * 4 / 1024);
return new Promise((resolve) => {
const du = spawn("du", ["-sk", dirPath], { stdio: [
"ignore",
"pipe",
"ignore"
] });
let output = "";
du.stdout.on("data", (chunk) => {
output += chunk.toString();
});
du.on("close", (code) => {
if (code !== 0) {
resolve(true);
return;
}
const match = /^(\d+)/.exec(output.trim());
if (!match) {
resolve(true);
return;
}
resolve(Number.parseInt(match[1], 10) <= heuristicKb);
});
du.on("error", () => {
resolve(true);
});
});
}
async function listTarEntries(tarBuffer) {
return new Promise((resolve) => {
const child = spawn("tar", ["-tzf", "-"], { stdio: [
"pipe",
"pipe",
"ignore"
] });
let stdoutBuf = "";
let aborted = false;
const watchdog = setTimeout(() => {
aborted = true;
try {
child.kill("SIGKILL");
} catch {}
resolve([]);
}, 1e4);
child.stdout.on("data", (chunk) => {
stdoutBuf += chunk.toString();
if (stdoutBuf.length > 32 * 1024 * 1024) {
aborted = true;
try {
child.kill("SIGKILL");
} catch {}
clearTimeout(watchdog);
resolve([]);
}
});
child.on("close", (code) => {
clearTimeout(watchdog);
if (aborted) return;
if (code !== 0) {
resolve([]);
return;
}
resolve(stdoutBuf.split("\n").map((line) => line.replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/$/u, "")).filter((line) => line.length > 0));
});
child.on("error", () => {
clearTimeout(watchdog);
if (!aborted) resolve([]);
});
child.stdin.end(tarBuffer);
});
}
async function listTreeEntries(root$1, maxEntries) {
const results = [];
const rootHandle = await root(root$1);
async function visit(relativeDir) {
const entries = await rootHandle.list(relativeDir, { withFileTypes: true });
entries.sort((left, right) => left.name.localeCompare(right.name));
for (const entry of entries) {
const rel = path.posix.join(relativeDir === "." ? "" : relativeDir, entry.name);
results.push(rel);
if (results.length > maxEntries) return false;
if (entry.isDirectory) {
if (!await visit(rel)) return false;
}
}
return true;
}
return await visit(".") ? results : "TOO_MANY";
}
async function handleDirFetch(params) {
const requestedPath = readAbsolutePath(params.path);
if (typeof requestedPath !== "string") return requestedPath;
const maxBytes = clampMaxBytes(params.maxBytes);
params.includeDotfiles;
const followSymlinks = params.followSymlinks === true;
const preflightOnly = params.preflightOnly === true;
const canonical = await resolveCanonicalReadPath({
requestedPath,
followSymlinks,
classifyError: classifyFsError,
notFoundMessage: "directory not found"
});
if (typeof canonical !== "string") return canonical;
const directory = await statRequiredDirectory(canonical, classifyFsError);
if (!directory.ok) return directory;
if (preflightOnly) try {
const entries = await listTreeEntries(canonical, 5e3);
if (entries === "TOO_MANY") return {
ok: false,
code: "TREE_TOO_LARGE",
message: "directory tree exceeds 5000 entries during preflight",
canonicalPath: canonical
};
return {
ok: true,
path: canonical,
tarBase64: "",
tarBytes: 0,
sha256: "",
fileCount: entries.length,
entries,
preflightOnly: true
};
} catch (err) {
return {
ok: false,
code: classifyFsError(err),
message: `preflight readdir failed: ${String(err)}`,
canonicalPath: canonical
};
}
if (!await preflightDu(canonical, maxBytes)) return {
ok: false,
code: "TREE_TOO_LARGE",
message: `directory tree exceeds estimated size limit (${maxBytes} bytes raw)`,
canonicalPath: canonical
};
const tarArgs = [
"-czf",
"-",
"-C",
canonical,
"."
];
const TAR_HARD_TIMEOUT_MS = 6e4;
const tarBuffer = await new Promise((resolve) => {
const child = spawn(process.platform !== "win32" ? "/usr/bin/tar" : "tar", tarArgs, { stdio: [
"ignore",
"pipe",
"pipe"
] });
const chunks = [];
let totalBytes = 0;
let aborted = false;
const watchdog = setTimeout(() => {
if (aborted) return;
aborted = true;
try {
child.kill("SIGKILL");
} catch {}
resolve("TIMEOUT");
}, TAR_HARD_TIMEOUT_MS);
child.stdout.on("data", (chunk) => {
if (aborted) return;
totalBytes += chunk.byteLength;
if (totalBytes > maxBytes) {
aborted = true;
clearTimeout(watchdog);
child.kill("SIGTERM");
resolve("TOO_LARGE");
return;
}
chunks.push(chunk);
});
child.on("close", (code) => {
clearTimeout(watchdog);
if (aborted) return;
if (code !== 0) {
resolve("ERROR");
return;
}
resolve(Buffer.concat(chunks));
});
child.on("error", () => {
clearTimeout(watchdog);
if (!aborted) resolve("ERROR");
});
});
if (tarBuffer === "TOO_LARGE") return {
ok: false,
code: "TREE_TOO_LARGE",
message: `tarball exceeded ${maxBytes} byte limit mid-stream`,
canonicalPath: canonical
};
if (tarBuffer === "TIMEOUT") return {
ok: false,
code: "READ_ERROR",
message: "tar command exceeded 60s wall-clock timeout (slow filesystem or symlink loop?)",
canonicalPath: canonical
};
if (tarBuffer === "ERROR") return {
ok: false,
code: "READ_ERROR",
message: "tar command failed",
canonicalPath: canonical
};
const sha256 = crypto.createHash("sha256").update(tarBuffer).digest("hex");
const tarBase64 = tarBuffer.toString("base64");
const tarBytes = tarBuffer.byteLength;
const entries = await listTarEntries(tarBuffer);
return {
ok: true,
path: canonical,
tarBase64,
tarBytes,
sha256,
fileCount: entries.length,
entries
};
}
//#endregion
export { handleDirFetch };