UNPKG

comfyui-mcp

Version:

Local-first, agent-native control plane for ComfyUI — MCP server + autonomous sidebar agent that drives your live graph in natural language on ANY LLM: Claude/ChatGPT/Gemini on your subscription (no API key), free local models via Ollama (fully offline),

111 lines • 4.77 kB
import { createReadStream } from "node:fs"; import { createWriteStream } from "node:fs"; import { stat } from "node:fs/promises"; import { pipeline } from "node:stream/promises"; import { ModelError, ValidationError } from "../../utils/errors.js"; import { requireOptionalDep } from "../../utils/optional-dep.js"; import { redactUrlForLogs } from "../download-auth.js"; import { bodyToReadable, safeErrorDetails, withPrefix } from "./utils.js"; async function loadAwsS3() { return requireOptionalDep("@aws-sdk/client-s3", { feature: "S3 uploads/downloads", installHint: "npm install @aws-sdk/client-s3", }); } export function isS3Url(url) { return url.startsWith("s3://"); } export function parseS3Url(url) { let parsed; try { parsed = new URL(url); } catch { throw new ValidationError("Invalid S3 URL. Expected s3://bucket/key."); } if (parsed.protocol !== "s3:" || !parsed.hostname || parsed.pathname.length <= 1) { throw new ValidationError("Invalid S3 URL. Expected s3://bucket/key."); } return { bucket: parsed.hostname, key: decodeURIComponent(parsed.pathname.slice(1)) }; } async function makeS3Client(auth) { const { S3Client } = await loadAwsS3(); const endpoint = auth?.endpoint ?? process.env.AWS_S3_ENDPOINT; const region = auth?.region ?? process.env.AWS_REGION ?? (endpoint ? "auto" : undefined); const config = { region, endpoint, forcePathStyle: Boolean(endpoint), }; const accessKeyId = auth?.access_key_id ?? process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = auth?.secret_access_key ?? process.env.AWS_SECRET_ACCESS_KEY; const sessionToken = auth?.session_token ?? process.env.AWS_SESSION_TOKEN; if (accessKeyId && secretAccessKey) { config.credentials = { accessKeyId, secretAccessKey, sessionToken }; } return new S3Client(config); } export async function downloadS3ToFile(url, targetPath, auth, signal) { const { bucket, key } = parseS3Url(url); const { GetObjectCommand } = await loadAwsS3(); const client = await makeS3Client(auth); try { // Thread the abort signal into BOTH the SDK request and the write pipeline so a // download_model action:"cancel" (#515) aborts the S3 transfer promptly instead of running to // completion. The streamUrlToFile cloud branch also guards before/after as a // backstop, so a cancelled transfer is never finalized even if the SDK ignores it. const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }), { abortSignal: signal, }); if (!response.Body) { throw new ModelError("S3 download response has no body", { url: redactUrlForLogs(url) }); } await pipeline(bodyToReadable(response.Body), createWriteStream(targetPath), { signal }); // #343 edge: the S3 stream can end early (dropped connection) and pipeline() // still resolves, leaving a truncated file. S3 tells us the authoritative // object size in ContentLength — verify the bytes on disk match before this // download can ever be reported as successful. const expected = typeof response.ContentLength === "number" ? response.ContentLength : 0; if (expected > 0) { const actual = (await stat(targetPath)).size; if (actual < expected) { throw new ModelError(`S3 download truncated: wrote ${actual} of ${expected} bytes — the stream ended early. Not complete; retry.`, { url: redactUrlForLogs(url) }); } } } catch (err) { if (err instanceof ModelError || err instanceof ValidationError) throw err; throw new ModelError("S3 download failed", { url: redactUrlForLogs(url), ...safeErrorDetails(err), }); } finally { client.destroy(); } } export async function uploadS3File(source, destination, auth) { const key = withPrefix(destination.prefix, source.filename); const { PutObjectCommand } = await loadAwsS3(); const client = await makeS3Client(auth); try { await client.send(new PutObjectCommand({ Bucket: destination.bucket, Key: key, Body: source.path ? createReadStream(source.path) : source.data, ContentType: source.contentType, })); return { provider: "s3", url: `s3://${destination.bucket}/${key}` }; } catch (err) { throw new ModelError("S3 upload failed", { url: `s3://${destination.bucket}/${key}`, ...safeErrorDetails(err), }); } finally { client.destroy(); } } //# sourceMappingURL=s3.js.map