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),

172 lines • 7.09 kB
import { createReadStream, 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 { safeErrorDetails, withPrefix } from "./utils.js"; const AZURE_BLOB_HOST_SUFFIX = ".blob.core.windows.net"; const AZURE_ACCOUNT_RE = /^[a-z0-9]{3,24}$/; async function loadAzure() { return requireOptionalDep("@azure/storage-blob", { feature: "Azure Blob storage uploads/downloads", installHint: "npm install @azure/storage-blob", }); } export function isAzureBlobUrl(url) { try { const parsed = new URL(url); return parsed.protocol === "https:" && accountFromBlobHost(parsed.hostname) !== undefined; } catch { return false; } } function hasQuery(url) { try { return new URL(url).search.length > 1; } catch { return false; } } function accountFromBlobHost(hostname) { if (!hostname.endsWith(AZURE_BLOB_HOST_SUFFIX)) return undefined; const account = hostname.slice(0, -AZURE_BLOB_HOST_SUFFIX.length); return AZURE_ACCOUNT_RE.test(account) ? account : undefined; } function accountFromConnectionString(connectionString) { const account = /(?:^|;)AccountName=([^;]+)/i.exec(connectionString)?.[1]; if (account) return account.toLowerCase(); const endpoint = /(?:^|;)BlobEndpoint=([^;]+)/i.exec(connectionString)?.[1]; if (!endpoint) return undefined; try { return accountFromBlobHost(new URL(endpoint).hostname); } catch { return undefined; } } async function blobServiceClientFromEnv() { const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING; if (connectionString) { const { BlobServiceClient } = await loadAzure(); return { account: accountFromConnectionString(connectionString), client: BlobServiceClient.fromConnectionString(connectionString), }; } const account = process.env.AZURE_STORAGE_ACCOUNT; const key = process.env.AZURE_STORAGE_KEY; if (account && key) { const { BlobServiceClient, StorageSharedKeyCredential } = await loadAzure(); const normalizedAccount = account.toLowerCase(); return { account: normalizedAccount, client: new BlobServiceClient(`https://${normalizedAccount}.blob.core.windows.net`, new StorageSharedKeyCredential(normalizedAccount, key)), }; } return undefined; } function parseAzureBlobUrl(url) { let parsed; try { parsed = new URL(url); } catch { throw new ValidationError("Invalid Azure Blob URL."); } const account = accountFromBlobHost(parsed.hostname); if (parsed.protocol !== "https:" || !account) { throw new ValidationError("Invalid Azure Blob URL. Expected https://<account>.blob.core.windows.net/<container>/<blob>."); } const parts = parsed.pathname.split("/").filter(Boolean); if (parts.length < 2) { throw new ValidationError("Invalid Azure Blob URL. Expected https://<account>.blob.core.windows.net/<container>/<blob>."); } return { account, container: parts[0], blob: parts.slice(1).join("/") }; } async function blobClientForDownload(url) { const parsed = parseAzureBlobUrl(url); const { BlobClient } = await loadAzure(); if (hasQuery(url)) { return new BlobClient(url); } const envClient = await blobServiceClientFromEnv(); if (envClient) { if (!envClient.account || envClient.account !== parsed.account) { throw new ValidationError("Azure Blob URL account must match configured Azure storage account."); } return envClient.client.getContainerClient(parsed.container).getBlobClient(parsed.blob); } return new BlobClient(url); } async function blobServiceClientForUpload() { const envClient = await blobServiceClientFromEnv(); if (!envClient) { throw new ValidationError("Azure upload requires AZURE_STORAGE_CONNECTION_STRING or AZURE_STORAGE_ACCOUNT plus AZURE_STORAGE_KEY."); } return envClient.client; } export async function downloadAzureBlobToFile(url, targetPath, signal) { try { // Thread the abort signal into BOTH the SDK download and the write pipeline so a // download_model action:"cancel" (#515) aborts the Azure transfer; the streamUrlToFile cloud // branch also guards before/after so a cancelled transfer is never finalized. const response = await (await blobClientForDownload(url)).download(undefined, undefined, { abortSignal: signal, }); if (!response.readableStreamBody) { throw new ModelError("Azure Blob download response has no body", { url: redactUrlForLogs(url), }); } await pipeline(response.readableStreamBody, createWriteStream(targetPath), { signal }); // #343 edge: verify the written size against the blob's authoritative // contentLength so an early-ending stream can't be reported as a complete // download (silent truncation). const expected = typeof response.contentLength === "number" ? response.contentLength : 0; if (expected > 0) { const actual = (await stat(targetPath)).size; if (actual < expected) { throw new ModelError(`Azure Blob 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("Azure Blob download failed", { url: redactUrlForLogs(url), ...safeErrorDetails(err), }); } } export async function uploadAzureBlobFile(source, destination) { const blobName = withPrefix(destination.blob_prefix, source.filename); try { const serviceClient = await blobServiceClientForUpload(); const blockBlobClient = serviceClient .getContainerClient(destination.container) .getBlockBlobClient(blobName); const options = source.contentType ? { blobHTTPHeaders: { blobContentType: source.contentType } } : undefined; if (source.path) { await blockBlobClient.uploadStream(createReadStream(source.path), undefined, undefined, options); } else { await blockBlobClient.uploadData(source.data ?? Buffer.alloc(0), options); } return { provider: "azure", url: redactUrlForLogs(blockBlobClient.url) }; } catch (err) { if (err instanceof ValidationError) throw err; throw new ModelError("Azure Blob upload failed", safeErrorDetails(err)); } } //# sourceMappingURL=azure-blob.js.map