diff --git a/custom_routes.py b/custom_routes.py index 40a5213..4416533 100644 --- a/custom_routes.py +++ b/custom_routes.py @@ -22,6 +22,8 @@ from logging.handlers import RotatingFileHandler from enum import Enum from urllib.parse import quote import threading +import hashlib +import aiohttp api = None api_task = None @@ -143,6 +145,138 @@ async def comfy_deploy_run(request): sockets = dict() +def get_comfyui_path_from_file_path(file_path): + file_path_parts = file_path.split("\\") + + if file_path_parts[0] == "input": + print("matching input") + file_path = os.path.join(folder_paths.get_directory_by_type("input"), *file_path_parts[1:]) + elif file_path_parts[0] == "models": + print("matching models") + file_path = folder_paths.get_full_path(file_path_parts[1], os.path.join(*file_path_parts[2:])) + + print(file_path) + + return file_path + +# Form ComfyUI Manager +def compute_sha256_checksum(filepath): + filepath = get_comfyui_path_from_file_path(filepath) + """Compute the SHA256 checksum of a file, in chunks""" + sha256 = hashlib.sha256() + with open(filepath, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b''): + sha256.update(chunk) + return sha256.hexdigest() + +# This is start uploading the files to Comfy Deploy +@server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file') +async def upload_file(request): + data = await request.json() + + file_path = data.get("file_path") + + print("Original file path", file_path) + + file_path = get_comfyui_path_from_file_path(file_path) + + # return web.json_response({ + # "error": f"File not uploaded" + # }, status=500) + + token = data.get("token") + get_url = data.get("url") + + try: + base = folder_paths.base_path + file_path = os.path.join(base, file_path) + + if os.path.exists(file_path): + file_size = os.path.getsize(file_path) + file_extension = os.path.splitext(file_path)[1] + + if file_extension in ['.jpg', '.jpeg']: + file_type = 'image/jpeg' + elif file_extension == '.png': + file_type = 'image/png' + elif file_extension == '.webp': + file_type = 'image/webp' + else: + file_type = 'application/octet-stream' # Default to binary file type if unknown + else: + return web.json_response({ + "error": f"File not found: {file_path}" + }, status=404) + + except Exception as e: + return web.json_response({ + "error": str(e) + }, status=500) + + if get_url: + try: + async with aiohttp.ClientSession() as session: + headers = {'Authorization': f'Bearer {token}'} + params = {'file_size': file_size, 'type': file_type} + async with session.get(get_url, params=params, headers=headers) as response: + if response.status == 200: + content = await response.json() + upload_url = content["upload_url"] + + with open(file_path, 'rb') as f: + headers = { + "Content-Type": file_type, + "x-amz-acl": "public-read", + "Content-Length": str(file_size) + } + async with session.put(upload_url, data=f, headers=headers) as upload_response: + if upload_response.status == 200: + return web.json_response({ + "message": "File uploaded successfully", + "download_url": content["download_url"] + }) + else: + return web.json_response({ + "error": f"Failed to upload file to {upload_url}. Status code: {upload_response.status}" + }, status=upload_response.status) + else: + return web.json_response({ + "error": f"Failed to fetch data from {get_url}. Status code: {response.status}" + }, status=response.status) + except Exception as e: + return web.json_response({ + "error": f"An error occurred while fetching data from {get_url}: {str(e)}" + }, status=500) + + return web.json_response({ + "error": f"File not uploaded" + }, status=500) + + +@server.PromptServer.instance.routes.get('/comfyui-deploy/get-file-hash') +async def get_file_hash(request): + file_path = request.rel_url.query.get('file_path', '') + + if file_path is None: + return web.json_response({ + "error": "file_path is required" + }, status=400) + + try: + base = folder_paths.base_path + file_path = os.path.join(base, file_path) + # print("file_path", file_path) + file_hash = compute_sha256_checksum( + file_path + ) + return web.json_response({ + "file_hash": file_hash + }) + except Exception as e: + return web.json_response({ + "error": str(e) + }, status=500) + @server.PromptServer.instance.routes.get('/comfyui-deploy/ws') async def websocket_handler(request): ws = web.WebSocketResponse() diff --git a/web-plugin/index.js b/web-plugin/index.js index f3bef7f..fc7dcb4 100644 --- a/web-plugin/index.js +++ b/web-plugin/index.js @@ -1,6 +1,7 @@ import { app } from "./app.js"; import { api } from "./api.js"; import { ComfyWidgets, LGraphNode } from "./widgets.js"; +import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.7"; /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @type {ComfyExtension} */ @@ -178,6 +179,71 @@ function showError(title, message) { ); } +function createDynamicUIHtml(data) { + let html = + '
'; + const bgcolor = "var(--comfy-input-bg)"; + const textColor = "var(--input-text)"; + + // Custom Nodes + html += `
`; + html += + '

Custom Nodes

'; + Object.values(data.custom_nodes).forEach((node) => { + html += ` +
+ ${ + node.name + } +

${node.hash}

+ ${ + node.warning + ? `

${node.warning}

` + : "" + } +
+ `; + }); + html += "
"; + + // Models + html += `
`; + html += + '

Models

'; + const modelSections = ["checkpoints", "ipadapter", "clip_vision"]; + modelSections.forEach((section) => { + html += ` +
+

${ + section.charAt(0).toUpperCase() + section.slice(1) + }

+ `; + data.models[section].forEach((item) => { + html += `

${item.name}

`; + }); + html += "
"; + }); + html += "
"; + + // Files + html += `
`; + html += + '

Files

'; + html += ` +
+

Images

+ `; + data.files.images.forEach((image) => { + html += `

${image.name}

`; + }); + html += "
"; + + html += "
"; + return html; +} + function addButton() { const menu = document.querySelector(".comfy-menu"); @@ -189,8 +255,29 @@ function addButton() { /** @type {LGraph} */ const graph = app.graph; + let { endpoint, apiKey, displayName } = getData(); + + if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { + configDialog.show(); + return; + } + + const ok = await confirmDialog.confirm( + "Confirm deployment -> " + displayName, + `A new version will be deployed, are you conform?

Include dependence`, + ); + if (!ok) return; + + const includeDeps = document.getElementById("include-deps").checked; + + if (endpoint.endsWith("/")) { + endpoint = endpoint.slice(0, -1); + } + loadingDialog.showLoading("Generating snapshot", "Please wait..."); + const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); // console.log(snapshot); + loadingDialog.close(); if (!snapshot) { showError( @@ -224,32 +311,94 @@ function addButton() { const deployMetaNode = deployMeta[0]; - // console.log(deployMetaNode); - const workflow_name = deployMetaNode.widgets[0].value; const workflow_id = deployMetaNode.widgets[1].value; - console.log(workflow_name, workflow_id); - const prompt = await app.graphToPrompt(); - console.log(graph); - console.log(prompt); + let deps = undefined; - // const endpoint = localStorage.getItem("endpoint") ?? ""; - // const apiKey = localStorage.getItem("apiKey"); + if (includeDeps) { + loadingDialog.showLoading("Fetching existing version", "Please wait..."); - const { endpoint, apiKey, displayName } = getData(); + const existing_workflow = await fetch( + endpoint + "/api/workflow/" + workflow_id, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + apiKey, + }, + }, + ) + .then((x) => x.json()) + .catch(() => { + return {}; + }); - if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { - configDialog.show(); - return; + loadingDialog.close(); + + loadingDialog.showLoading( + "Generating dependency graph", + "Please wait...", + ); + deps = await generateDependencyGraph( + prompt.output, + snapshot, + async (file) => { + console.log(file); + loadingDialog.showLoading("Generating hash", file); + const hash = await fetch( + `/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent( + file, + )}`, + ).then((x) => x.json()); + loadingDialog.showLoading("Generating hash", file); + console.log(hash); + return hash.file_hash; + }, + async (file, hash, prevhash) => { + console.log("Uploading ", file); + loadingDialog.showLoading("Uploading file", file); + try { + const { download_url } = await fetch( + `/comfyui-deploy/upload-file`, + { + method: "POST", + body: JSON.stringify({ + file_path: file, + token: apiKey, + url: endpoint + "/api/upload-url", + }), + }, + ) + .then((x) => x.json()) + .catch(() => { + loadingDialog.close(); + confirmDialog.confirm("Error", "Unable to upload file " + file); + }); + loadingDialog.showLoading("Uploaded file", file); + console.log(download_url); + return download_url; + } catch (error) { + return undefined; + } + }, + existing_workflow.dependencies, + ); + + loadingDialog.close(); + + const depsOk = await confirmDialog.confirm( + "Check dependencies", + // JSON.stringify(deps, null, 2), + createDynamicUIHtml(deps), + ); + if (!depsOk) return; + + console.log(deps); } - const ok = await confirmDialog.confirm( - "Confirm deployment -> " + displayName, - "A new version will be deployed, are you conform?", - ); - if (!ok) return; + loadingDialog.showLoading("Deploying..."); title.innerText = "Deploying..."; title.style.color = "orange"; @@ -261,6 +410,8 @@ function addButton() { endpoint = endpoint.slice(0, -1); } + // console.log(prompt.workflow); + const apiRoute = endpoint + "/api/workflow"; // const userId = apiKey try { @@ -270,6 +421,7 @@ function addButton() { workflow: prompt.workflow, workflow_api: prompt.output, snapshot: snapshot, + dependencies: deps, }; console.log(body); let data = await fetch(apiRoute, { @@ -289,6 +441,8 @@ function addButton() { data = await data.json(); } + loadingDialog.close(); + title.textContent = "Done"; title.style.color = "green"; @@ -305,6 +459,7 @@ function addButton() { title.style.color = "white"; }, 1000); } catch (e) { + loadingDialog.close(); app.ui.dialog.show(e); console.error(e); title.textContent = "Error"; @@ -381,7 +536,7 @@ export class InfoDialog extends ComfyDialog { showMessage(title, message) { this.show(` -
+

${title}