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

190 lines • 9.98 kB
/** * #1477 — a download filled the SYSTEM drive and would have driven it to zero. * * `download_model` stages the whole file in the content-addressed cache before it * lands at its destination, and the cache path comes only from `homedir()`: * * const DEFAULT_CACHE_DIR = join(homedir(), ".comfyui-mcp", "cache"); * * On Windows `homedir()` is essentially always the system drive, while ComfyUI models * are almost always on a big secondary volume. The reporter's ComfyUI lives on `F:` * with 1 TB free; `C:` had 0.7 GB free of 232 GB. A 32.29 GB download to the roomy * volume was written to the full one, and the `.partial` reached 22.62 GB before it * died. * * ## Why this is worse than a failed download * * Taking a Windows system drive to zero risks the page file and general OS stability, * not just this transfer. The failure is not confined to the thing that failed. * * ## What this module does, and deliberately does not do * * It REFUSES up front when the staging volume cannot hold what we already know is * coming — `Content-Length` is parsed before the first byte is written, so the check * costs nothing and needs no new network work. * * It does NOT relocate the cache. Choosing a staging directory on the destination * volume is the deeper fix the reporter also asks for, and it changes where files * land, how the cache is addressed, and whether the final move is a rename or a copy. * That deserves its own change. Refusing before the disk fills is the part that stops * the harm, and it is the part that can be made safe now. * * Every check here FAILS SOFT. If free space cannot be measured, the download proceeds * exactly as before: this exists to prevent a known-bad write, never to invent a new * way for a good one to be blocked. */ import { statfs } from "node:fs/promises"; import { dirname, parse, resolve } from "node:path"; /** * Free bytes on the volume holding `path`, or `null` when it cannot be determined. * * `statfs` throws ENOENT for a path that does not exist yet, and the cache directory * routinely does not on a first run — so walk up to the nearest existing ancestor, * which is on the same volume by construction. Give up at the filesystem root rather * than looping. */ export async function volumeSpaceFor(path) { let dir = resolve(path); const { root } = parse(dir); for (;;) { try { const st = await statfs(dir); const free = Number(st.bsize) * Number(st.bavail); const total = Number(st.bsize) * Number(st.blocks); return Number.isFinite(free) && free >= 0 ? { free, total } : null; } catch (err) { // Climb ONLY for "this path does not exist yet", which is the case that makes // the walk necessary at all (a first run has no cache directory). Review // caught the original catch-all: on a junction, mount point or network share // whose statfs fails for PERMISSION or transport reasons, climbing would // silently measure a DIFFERENT volume and answer confidently about the wrong // disk -- which could permit the very overflow this exists to stop. const code = err?.code; if (code !== "ENOENT" && code !== "ENOTDIR") return null; const parent = dirname(dir); // `dirname` of a root returns the root: that is the stop condition, and without // it a missing drive letter would spin forever. if (parent === dir || dir === root) return null; dir = parent; } } } export async function freeBytesFor(path) { return (await volumeSpaceFor(path))?.free ?? null; } /** Bytes left free after a download, below which we refuse to start it. * * Not zero: landing a volume on exactly 0 free is its own failure, and on the system * drive it is the page-file case this issue is about. One GiB is small enough not to * block a legitimate tight-but-workable download and large enough to leave the OS * somewhere to breathe. */ export const VOLUME_HEADROOM_BYTES = 1024 ** 3; /** * The reserve to keep free on a volume of `total` bytes. * * A flat 1 GiB is right for the case this issue is about -- a 232 GB system drive * whose page file needs somewhere to live -- and wrong for a small or removable * volume, where it would refuse a download that fits comfortably. Review flagged * exactly that. So the reserve is the SMALLER of 1 GiB and 5% of the volume, which * leaves the system-drive protection untouched and stops a 4 GB stick being declared * unusable for a 2 GB file. */ export function headroomFor(total) { if (!Number.isFinite(total) || total <= 0) return VOLUME_HEADROOM_BYTES; return Math.min(VOLUME_HEADROOM_BYTES, Math.floor(total * 0.05)); } const gb = (n) => `${(n / 1024 ** 3).toFixed(2)} GB`; /** * The refusal for a download whose staging volume cannot hold it. * * `destFree` is the free space on the DESTINATION volume when that is a different * volume from the cache. When the destination has room and the cache does not, that * is the whole story and the message says so — it turns a fatal error into one * setting. */ export function insufficientCacheSpaceMessage(opts) { const { needBytes, cacheDir, cacheFree, destDir, destFree, resuming = false, partialExists = false, } = opts; // The SAME reserve the policy applies, not the flat one: review found the message // demanding 1 GiB of the destination while `checkCacheVolumeSpace` would have // accepted a 5% reserve, so a destination that fits could be reported as too small. // A remediation hint that contradicts the policy is worse than no hint. const destHasRoom = typeof destFree === "number" && destFree >= needBytes + (opts.destHeadroom ?? VOLUME_HEADROOM_BYTES); const head = `NOT downloaded: this needs ${gb(needBytes)} of staging space and the download ` + `cache volume has ${gb(cacheFree)} free (${cacheDir}).`; // The single most useful fact, when it is true: the file was always going somewhere // that fits, and only the staging location does not. const split = destHasRoom ? ` The DESTINATION has room — ${gb(destFree)} free at ${destDir} — so ` + `this is not a "your disk is full" problem: the model is staged in the cache ` + `BEFORE it lands, and the cache is on a different volume.` : destDir && typeof destFree === "number" ? ` The destination volume does not have room either (${gb(destFree)} free at ` + `${destDir}), so a different cache location alone will not resolve this.` : ""; // Review caught this claiming too much: the check also runs on a RESUME, where a // .partial already exists on disk. Telling that user "nothing was downloaded" is // false and invites them to delete a partial they could have reused. const state = resuming ? ` Your existing partial download is untouched and still resumable — this ` + `refusal happens before any further bytes are written.` : partialExists ? ` A partial download from an earlier attempt is still on disk; this refusal ` + `happens before any further bytes are written, so it is unchanged. Freeing ` + `space may require removing it, and the figure above does NOT assume it was ` + `reclaimed.` : ` Nothing was written and nothing was partially downloaded — this refusal ` + `happens before the first byte, precisely so a full volume is not driven to ` + `zero (#1477).`; const fix = ` Point the cache at a volume with room by setting COMFYUI_DOWNLOAD_CACHE_DIR ` + `(for example to a directory beside your models), then retry.` + state; return head + split + fix; } /** * Decide whether staging `needBytes` at `cacheDir` is safe. Returns a refusal string, * or `null` to proceed. * * Proceeds when the size is unknown or free space cannot be read: an unmeasurable * volume must not become an unusable one. */ export async function checkCacheVolumeSpace(opts) { const { needBytes, cacheDir, destDir, resuming, partialExists } = opts; if (!needBytes || !Number.isFinite(needBytes) || needBytes <= 0) return null; const space = await volumeSpaceFor(cacheDir); if (space === null) return null; const headroom = headroomFor(space.total); if (space.free >= needBytes + headroom) return null; const destSpace = destDir ? await volumeSpaceFor(destDir) : null; const destFree = destSpace?.free ?? null; return insufficientCacheSpaceMessage({ needBytes, cacheDir, cacheFree: space.free, destDir, destFree, destHeadroom: destSpace ? headroomFor(destSpace.total) : undefined, resuming, partialExists, }); } /* * DELIBERATELY CONSERVATIVE: a restart that is about to truncate a stale .partial * will reclaim its bytes, and crediting them here would let a few more downloads * through. It is not done, because the truncation happens on two different paths — * one BEFORE the request (a declined full response) and one at the write flags AFTER * this check — and on the first of those the space is already free, so crediting it * would DOUBLE-COUNT and permit exactly the overflow this module exists to stop. * * The two error directions are not symmetric. A false refusal costs one retry after * setting COMFYUI_DOWNLOAD_CACHE_DIR; a false allow costs the system drive. Until the * ordering is established by measurement rather than by reading, this stays on the * side that cannot cause the harm — and the message above says plainly that the * figure does not assume the partial was reclaimed, so the number is never misread. */ //# sourceMappingURL=download-volume.js.map