import configparser import mimetypes import shutil import folder_paths import os import sys import threading import subprocess def handle_stream(stream, prefix): for line in stream: print(prefix, line, end="") def run_script(cmd, cwd='.'): process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1) stdout_thread = threading.Thread(target=handle_stream, args=(process.stdout, "")) stderr_thread = threading.Thread(target=handle_stream, args=(process.stderr, "[!]")) stdout_thread.start() stderr_thread.start() stdout_thread.join() stderr_thread.join() return process.wait() try: import git except: my_path = os.path.dirname(__file__) requirements_path = os.path.join(my_path, "requirements.txt") print(f"## ComfyUI-Manager: installing dependencies") run_script([sys.executable, '-s', '-m', 'pip', 'install', '-r', requirements_path]) try: import git except: print(f"## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.") run_script([sys.executable, '-s', '-m', 'pip', 'install', '--user', '-r', requirements_path]) try: import git except: print(f"## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)") print(f"## ComfyUI-Manager: installing dependencies done.") sys.path.append('../..') from torchvision.datasets.utils import download_url # ensure .js print("### Loading: ComfyUI-Manager (V0.36)") comfy_ui_required_revision = 1240 comfy_ui_revision = "Unknown" comfy_path = os.path.dirname(folder_paths.__file__) custom_nodes_path = os.path.join(comfy_path, 'custom_nodes') js_path = os.path.join(comfy_path, "web", "extensions") comfyui_manager_path = os.path.dirname(__file__) local_db_model = os.path.join(comfyui_manager_path, "model-list.json") local_db_alter = os.path.join(comfyui_manager_path, "alter-list.json") local_db_custom_node_list = os.path.join(comfyui_manager_path, "custom-node-list.json") local_db_extension_node_mappings = os.path.join(comfyui_manager_path, "extension-node-map.json") git_script_path = os.path.join(os.path.dirname(__file__), "git_helper.py") startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts") config_path = os.path.join(os.path.dirname(__file__), "config.ini") cached_config = None default_channels = 'default::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main,recent::https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/node_db/new,' with open(os.path.join(comfyui_manager_path, 'channels.list'), 'r') as file: channels = file.read() default_channels = channels.replace('\n', ',') from comfy.cli_args import args import latent_preview def write_config(): config = configparser.ConfigParser() config['default'] = { 'preview_method': get_current_preview_method(), 'badge_mode': get_config()['badge_mode'], 'git_exe': get_config()['git_exe'], 'channel_url': get_config()['channel_url'], 'channel_url_list': get_config()['channel_url_list'] } with open(config_path, 'w') as configfile: config.write(configfile) def read_config(): try: config = configparser.ConfigParser() config.read(config_path) default_conf = config['default'] channel_url_list_is_valid = True if 'channel_url_list' in default_conf and default_conf['channel_url_list'] != '': for item in default_conf['channel_url_list'].split(","): if len(item.split("::")) != 2: channel_url_list_is_valid = False break if channel_url_list_is_valid: ch_url_list = default_conf['channel_url_list'] else: print(f"[WARN] ComfyUI-Manager: channel_url_list is invalid format") ch_url_list = '' return { 'preview_method': default_conf['preview_method'] if 'preview_method' in default_conf else get_current_preview_method(), 'badge_mode': default_conf['badge_mode'] if 'badge_mode' in default_conf else 'none', 'git_exe': default_conf['git_exe'] if 'git_exe' in default_conf else '', 'channel_url': default_conf['channel_url'] if 'channel_url' in default_conf else 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main', 'channel_url_list': ch_url_list } except Exception: return { 'preview_method': get_current_preview_method(), 'badge_mode': 'none', 'git_exe': '', 'channel_url': 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main', 'channel_url_list': '' } def get_config(): global cached_config if cached_config is None: cached_config = read_config() return cached_config def get_current_preview_method(): if args.preview_method == latent_preview.LatentPreviewMethod.Auto: return "auto" elif args.preview_method == latent_preview.LatentPreviewMethod.Latent2RGB: return "latent2rgb" elif args.preview_method == latent_preview.LatentPreviewMethod.TAESD: return "taesd" else: return "none" def set_preview_method(method): if method == 'auto': args.preview_method = latent_preview.LatentPreviewMethod.Auto elif method == 'latent2rgb': args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB elif method == 'taesd': args.preview_method = latent_preview.LatentPreviewMethod.TAESD else: args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews get_config()['preview_method'] = args.preview_method def set_badge_mode(mode): get_config()['badge_mode'] = mode set_preview_method(get_config()['preview_method']) def try_install_script(url, repo_path, install_cmd): int_comfyui_revision = 0 if type(comfy_ui_revision) == int: int_comfyui_revision = comfy_ui_revision elif comfy_ui_revision.isdigit(): int_comfyui_revision = int(comfy_ui_revision) if platform.system() == "Windows" and int_comfyui_revision >= comfy_ui_required_revision: if not os.path.exists(startup_script_path): os.makedirs(startup_script_path) script_path = os.path.join(startup_script_path, "install-scripts.txt") with open(script_path, "a") as file: obj = [repo_path] + install_cmd file.write(f"{obj}\n") return True else: print(f"\n## ComfyUI-Manager: EXECUTE => {install_cmd}") code = run_script(install_cmd, cwd=repo_path) if platform.system() == "Windows": try: if int(comfy_ui_revision) < comfy_ui_required_revision: print("\n\n###################################################################") print(f"[WARN] ComfyUI-Manager: Your ComfyUI version ({comfy_ui_revision}) is too old. Please update to the latest version.") print(f"[WARN] The extension installation feature may not work properly in the current installed ComfyUI version on Windows environment.") print("###################################################################\n\n") except: pass if code != 0: print(f"install script failed: {url}") return False def print_comfyui_version(): global comfy_ui_revision try: repo = git.Repo(os.path.dirname(folder_paths.__file__)) comfy_ui_revision = len(list(repo.iter_commits('HEAD'))) current_branch = repo.active_branch.name git_hash = repo.head.commit.hexsha try: if int(comfy_ui_revision) < comfy_ui_required_revision: print(f"\n\n## [WARN] ComfyUI-Manager: Your ComfyUI version ({comfy_ui_revision}) is too old. Please update to the latest version. ##\n\n") except: pass if current_branch == "master": print(f"### ComfyUI Revision: {comfy_ui_revision} [{git_hash[:8]}] | Released on '{repo.head.commit.committed_datetime.date()}'") else: print(f"### ComfyUI Revision: {comfy_ui_revision} on '{current_branch}' [{git_hash[:8]}] | Released on '{repo.head.commit.committed_datetime.date()}'") except: print("### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)") print_comfyui_version() # use subprocess to avoid file system lock by git (Windows) def __win_check_git_update(path, do_fetch=False, do_update=False): if do_fetch: command = [sys.executable, git_script_path, "--fetch", path] elif do_update: command = [sys.executable, git_script_path, "--pull", path] else: command = [sys.executable, git_script_path, "--check", path] process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, _ = process.communicate() output = output.decode('utf-8').strip() if do_update: if "CUSTOM NODE PULL: True" in output: process.wait() print(f"\rUpdated: {path}") return True elif "CUSTOM NODE PULL: None" in output: process.wait() return True else: print(f"\rUpdate error: {path}") process.wait() return False else: if "CUSTOM NODE CHECK: True" in output: process.wait() return True elif "CUSTOM NODE CHECK: False" in output: process.wait() return False else: print(f"\rFetch error: {path}") process.wait() return False def __win_check_git_pull(path): command = [sys.executable, git_script_path, "--pull", path] process = subprocess.Popen(command) process.wait() def git_repo_has_updates(path, do_fetch=False, do_update=False): if do_fetch: print(f"\x1b[2K\rFetching: {path}", end='') elif do_update: print(f"\x1b[2K\rUpdating: {path}", end='') # Check if the path is a git repository if not os.path.exists(os.path.join(path, '.git')): raise ValueError('Not a git repository') if platform.system() == "Windows": return __win_check_git_update(path, do_fetch, do_update) else: # Fetch the latest commits from the remote repository repo = git.Repo(path) current_branch = repo.active_branch branch_name = current_branch.name remote_name = 'origin' remote = repo.remote(name=remote_name) # Get the current commit hash commit_hash = repo.head.commit.hexsha if do_fetch or do_update: remote.fetch() if do_update: try: remote.pull(rebase=True) repo.git.submodule('update', '--init', '--recursive') new_commit_hash = repo.head.commit.hexsha if commit_hash != new_commit_hash: print(f"\x1b[2K\rUpdated: {path}") return True else: return False except Exception as e: print(f"\nUpdating failed: {path}\n{e}", file=sys.stderr) # Get commit hash of the remote branch remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha # Compare the commit hashes to determine if the local repository is behind the remote repository if commit_hash != remote_commit_hash: # Get the commit dates commit_date = repo.head.commit.committed_datetime remote_commit_date = repo.refs[f'{remote_name}/{branch_name}'].object.committed_datetime # Compare the commit dates to determine if the local repository is behind the remote repository if commit_date < remote_commit_date: return True return False def git_pull(path): # Check if the path is a git repository if not os.path.exists(os.path.join(path, '.git')): raise ValueError('Not a git repository') # Pull the latest changes from the remote repository if platform.system() == "Windows": return __win_check_git_pull(path) else: repo = git.Repo(path) if repo.is_dirty(): repo.git.stash() origin = repo.remote(name='origin') origin.pull(rebase=True) repo.git.submodule('update', '--init', '--recursive') repo.close() return True async def get_data(uri): print(f"FETCH DATA from: {uri}") if uri.startswith("http"): async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: async with session.get(uri) as resp: json_text = await resp.text() else: with open(uri, "r", encoding="utf-8") as f: json_text = f.read() json_obj = json.loads(json_text) return json_obj def setup_js(): import nodes js_dest_path = os.path.join(js_path, "comfyui-manager") if hasattr(nodes, "EXTENSION_WEB_DIRS"): if os.path.exists(js_dest_path): shutil.rmtree(js_dest_path) else: print(f"[WARN] ComfyUI-Manager: Your ComfyUI version is outdated. Please update to the latest version.") # setup js if not os.path.exists(js_dest_path): os.makedirs(js_dest_path) js_src_path = os.path.join(comfyui_manager_path, "js", "comfyui-manager.js") print(f"### ComfyUI-Manager: Copy .js from '{js_src_path}' to '{js_dest_path}'") shutil.copy(js_src_path, js_dest_path) setup_js() def setup_environment(): git_exe = get_config()['git_exe'] if git_exe != '': git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=git_exe) setup_environment() # Expand Server api import server from aiohttp import web import aiohttp import json import zipfile import urllib.request def get_model_dir(data): if data['save_path'] != 'default': if '..' in data['save_path'] or data['save_path'].startswith('/'): print(f"[WARN] '{data['save_path']}' is not allowed path. So it will be saved into 'models/etc'.") base_model = "etc" else: if data['save_path'].startswith("custom_nodes"): base_model = os.path.join(comfy_path, data['save_path']) else: base_model = os.path.join(folder_paths.models_dir, data['save_path']) else: model_type = data['type'] if model_type == "checkpoints": base_model = folder_paths.folder_names_and_paths["checkpoints"][0][0] elif model_type == "unclip": base_model = folder_paths.folder_names_and_paths["checkpoints"][0][0] elif model_type == "VAE": base_model = folder_paths.folder_names_and_paths["vae"][0][0] elif model_type == "lora": base_model = folder_paths.folder_names_and_paths["loras"][0][0] elif model_type == "T2I-Adapter": base_model = folder_paths.folder_names_and_paths["controlnet"][0][0] elif model_type == "T2I-Style": base_model = folder_paths.folder_names_and_paths["controlnet"][0][0] elif model_type == "controlnet": base_model = folder_paths.folder_names_and_paths["controlnet"][0][0] elif model_type == "clip_vision": base_model = folder_paths.folder_names_and_paths["clip_vision"][0][0] elif model_type == "gligen": base_model = folder_paths.folder_names_and_paths["gligen"][0][0] elif model_type == "upscale": base_model = folder_paths.folder_names_and_paths["upscale_models"][0][0] elif model_type == "embeddings": base_model = folder_paths.folder_names_and_paths["embeddings"][0][0] else: base_model = "etc" return base_model def get_model_path(data): base_model = get_model_dir(data) return os.path.join(base_model, data['filename']) def check_a_custom_node_installed(item, do_fetch=False, do_update_check=True, do_update=False): item['installed'] = 'None' if item['install_type'] == 'git-clone' and len(item['files']) == 1: url = item['files'][0] if url.endswith("/"): url = url[:-1] dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") dir_path = os.path.join(custom_nodes_path, dir_name) if os.path.exists(dir_path): try: if do_update_check and git_repo_has_updates(dir_path, do_fetch, do_update): item['installed'] = 'Update' else: item['installed'] = 'True' except: item['installed'] = 'True' elif os.path.exists(dir_path + ".disabled"): item['installed'] = 'Disabled' else: item['installed'] = 'False' elif item['install_type'] == 'copy' and len(item['files']) == 1: dir_name = os.path.basename(item['files'][0]) if item['files'][0].endswith('.py'): base_path = custom_nodes_path elif 'js_path' in item: base_path = os.path.join(js_path, item['js_path']) else: base_path = js_path file_path = os.path.join(base_path, dir_name) if os.path.exists(file_path): item['installed'] = 'True' elif os.path.exists(file_path + ".disabled"): item['installed'] = 'Disabled' else: item['installed'] = 'False' def check_custom_nodes_installed(json_obj, do_fetch=False, do_update_check=True, do_update=False): if do_fetch: print("Start fetching...", end="") elif do_update: print("Start updating...", end="") elif do_update_check: print("Start update check...", end="") for item in json_obj['custom_nodes']: check_a_custom_node_installed(item, do_fetch, do_update_check, do_update) if do_fetch: print(f"\x1b[2K\rFetching done.") elif do_update: update_exists = any(item['installed'] == 'Update' for item in json_obj['custom_nodes']) if update_exists: print(f"\x1b[2K\rUpdate done.") else: print(f"\x1b[2K\rAll extensions are already up-to-date.") elif do_update_check: print(f"\x1b[2K\rUpdate check done.") @server.PromptServer.instance.routes.get("/customnode/getmappings") async def fetch_customnode_mappings(request): if request.rel_url.query["mode"] == "local": uri = local_db_extension_node_mappings else: uri = get_config()['channel_url'] + '/extension-node-map.json' json_obj = await get_data(uri) return web.json_response(json_obj, content_type='application/json') @server.PromptServer.instance.routes.get("/customnode/fetch_updates") async def fetch_updates(request): try: if request.rel_url.query["mode"] == "local": uri = local_db_custom_node_list else: uri = get_config()['channel_url'] + '/custom-node-list.json' json_obj = await get_data(uri) check_custom_nodes_installed(json_obj, True) update_exists = any('custom_nodes' in json_obj and 'installed' in node and node['installed'] == 'Update' for node in json_obj['custom_nodes']) if update_exists: return web.Response(status=201) return web.Response(status=200) except: return web.Response(status=400) @server.PromptServer.instance.routes.get("/customnode/update_all") async def update_all(request): try: if request.rel_url.query["mode"] == "local": uri = local_db_custom_node_list else: uri = get_config()['channel_url'] + '/custom-node-list.json' json_obj = await get_data(uri) check_custom_nodes_installed(json_obj, do_update=True) update_exists = any(item['installed'] == 'Update' for item in json_obj['custom_nodes']) if update_exists: return web.Response(status=201) return web.Response(status=200) except: return web.Response(status=400) @server.PromptServer.instance.routes.get("/customnode/getlist") async def fetch_customnode_list(request): if "skip_update" in request.rel_url.query and request.rel_url.query["skip_update"] == "true": skip_update = True else: skip_update = False if request.rel_url.query["mode"] == "local": uri = local_db_custom_node_list else: uri = get_config()['channel_url'] + '/custom-node-list.json' json_obj = await get_data(uri) check_custom_nodes_installed(json_obj, False, not skip_update) return web.json_response(json_obj, content_type='application/json') @server.PromptServer.instance.routes.get("/alternatives/getlist") async def fetch_alternatives_list(request): if "skip_update" in request.rel_url.query and request.rel_url.query["skip_update"] == "true": skip_update = True else: skip_update = False if request.rel_url.query["mode"] == "local": uri1 = local_db_alter uri2 = local_db_custom_node_list else: uri1 = get_config()['channel_url'] + '/alter-list.json' uri2 = get_config()['channel_url'] + '/custom-node-list.json' alter_json = await get_data(uri1) custom_node_json = await get_data(uri2) fileurl_to_custom_node = {} for item in custom_node_json['custom_nodes']: for fileurl in item['files']: fileurl_to_custom_node[fileurl] = item for item in alter_json['items']: fileurl = item['id'] if fileurl in fileurl_to_custom_node: custom_node = fileurl_to_custom_node[fileurl] check_a_custom_node_installed(custom_node, not skip_update) item['custom_node'] = custom_node return web.json_response(alter_json, content_type='application/json') def check_model_installed(json_obj): for item in json_obj['models']: item['installed'] = 'None' model_path = get_model_path(item) if model_path is not None: if os.path.exists(model_path): item['installed'] = 'True' else: item['installed'] = 'False' @server.PromptServer.instance.routes.get("/externalmodel/getlist") async def fetch_externalmodel_list(request): if request.rel_url.query["mode"] == "local": uri = local_db_model else: uri = get_config()['channel_url'] + '/model-list.json' json_obj = await get_data(uri) check_model_installed(json_obj) return web.json_response(json_obj, content_type='application/json') def unzip_install(files): temp_filename = 'manager-temp.zip' for url in files: if url.endswith("/"): url = url[:-1] try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'} req = urllib.request.Request(url, headers=headers) response = urllib.request.urlopen(req) data = response.read() with open(temp_filename, 'wb') as f: f.write(data) with zipfile.ZipFile(temp_filename, 'r') as zip_ref: zip_ref.extractall(custom_nodes_path) os.remove(temp_filename) except Exception as e: print(f"Install(unzip) error: {url} / {e}", file=sys.stderr) return False print("Installation was successful.") return True def download_url_with_agent(url, save_path): try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'} req = urllib.request.Request(url, headers=headers) response = urllib.request.urlopen(req) data = response.read() if not os.path.exists(os.path.dirname(save_path)): os.makedirs(os.path.dirname(save_path)) with open(save_path, 'wb') as f: f.write(data) except Exception as e: print(f"Download error: {url} / {e}", file=sys.stderr) return False print("Installation was successful.") return True def copy_install(files, js_path_name=None): for url in files: if url.endswith("/"): url = url[:-1] try: if url.endswith(".py"): download_url(url, custom_nodes_path) else: path = os.path.join(js_path, js_path_name) if js_path_name is not None else js_path if not os.path.exists(path): os.makedirs(path) download_url(url, path) except Exception as e: print(f"Install(copy) error: {url} / {e}", file=sys.stderr) return False print("Installation was successful.") return True def copy_uninstall(files, js_path_name='.'): for url in files: if url.endswith("/"): url = url[:-1] dir_name = os.path.basename(url) base_path = custom_nodes_path if url.endswith('.py') else os.path.join(js_path, js_path_name) file_path = os.path.join(base_path, dir_name) try: if os.path.exists(file_path): os.remove(file_path) elif os.path.exists(file_path + ".disabled"): os.remove(file_path + ".disabled") except Exception as e: print(f"Uninstall(copy) error: {url} / {e}", file=sys.stderr) return False print("Uninstallation was successful.") return True def copy_set_active(files, is_disable, js_path_name='.'): if is_disable: action_name = "Disable" else: action_name = "Enable" for url in files: if url.endswith("/"): url = url[:-1] dir_name = os.path.basename(url) base_path = custom_nodes_path if url.endswith('.py') else os.path.join(js_path, js_path_name) file_path = os.path.join(base_path, dir_name) try: if is_disable: current_name = file_path new_name = file_path + ".disabled" else: current_name = file_path + ".disabled" new_name = file_path os.rename(current_name, new_name) except Exception as e: print(f"{action_name}(copy) error: {url} / {e}", file=sys.stderr) return False print(f"{action_name} was successful.") return True def execute_install_script(url, repo_path): install_script_path = os.path.join(repo_path, "install.py") requirements_path = os.path.join(repo_path, "requirements.txt") if os.path.exists(requirements_path): print("Install: pip packages") with open(requirements_path, "r") as requirements_file: for line in requirements_file: package_name = line.strip() if package_name: install_cmd = [sys.executable, "-m", "pip", "install", package_name] if package_name.strip() != "": try_install_script(url, repo_path, install_cmd) if os.path.exists(install_script_path): print(f"Install: install script") install_cmd = [sys.executable, "install.py"] try_install_script(url, repo_path, install_cmd) return True def gitclone_install(files): print(f"install: {files}") for url in files: if url.endswith("/"): url = url[:-1] try: print(f"Download: git clone '{url}'") repo_name = os.path.splitext(os.path.basename(url))[0] repo_path = os.path.join(custom_nodes_path, repo_name) # Clone the repository from the remote URL if platform.system() == 'Windows': run_script([sys.executable, git_script_path, "--clone", custom_nodes_path, url]) else: repo = git.Repo.clone_from(url, repo_path, recursive=True) repo.git.clear_cache() repo.close() if not execute_install_script(url, repo_path): return False except Exception as e: print(f"Install(git-clone) error: {url} / {e}", file=sys.stderr) return False print("Installation was successful.") return True import platform import subprocess import time def rmtree(path): retry_count = 3 while True: try: retry_count -= 1 if platform.system() == "Windows": run_script(['attrib', '-R', path + '\\*', '/S']) shutil.rmtree(path) return True except Exception as ex: print(f"ex: {ex}") time.sleep(3) if retry_count < 0: raise ex print(f"Uninstall retry({retry_count})") def gitclone_uninstall(files): import shutil import os print(f"uninstall: {files}") for url in files: if url.endswith("/"): url = url[:-1] try: dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") dir_path = os.path.join(custom_nodes_path, dir_name) # safety check if dir_path == '/' or dir_path[1:] == ":/" or dir_path == '': print(f"Uninstall(git-clone) error: invalid path '{dir_path}' for '{url}'") return False install_script_path = os.path.join(dir_path, "uninstall.py") disable_script_path = os.path.join(dir_path, "disable.py") if os.path.exists(install_script_path): uninstall_cmd = [sys.executable, "uninstall.py"] code = run_script(uninstall_cmd, cwd=dir_path) if code != 0: print(f"An error occurred during the execution of the uninstall.py script. Only the '{dir_path}' will be deleted.") elif os.path.exists(disable_script_path): disable_script = [sys.executable, "disable.py"] code = run_script(disable_script, cwd=dir_path) if code != 0: print(f"An error occurred during the execution of the disable.py script. Only the '{dir_path}' will be deleted.") if os.path.exists(dir_path): rmtree(dir_path) elif os.path.exists(dir_path + ".disabled"): rmtree(dir_path + ".disabled") except Exception as e: print(f"Uninstall(git-clone) error: {url} / {e}", file=sys.stderr) return False print("Uninstallation was successful.") return True def gitclone_set_active(files, is_disable): import os if is_disable: action_name = "Disable" else: action_name = "Enable" print(f"{action_name}: {files}") for url in files: if url.endswith("/"): url = url[:-1] try: dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") dir_path = os.path.join(custom_nodes_path, dir_name) # safey check if dir_path == '/' or dir_path[1:] == ":/" or dir_path == '': print(f"{action_name}(git-clone) error: invalid path '{dir_path}' for '{url}'") return False if is_disable: current_path = dir_path new_path = dir_path + ".disabled" else: current_path = dir_path + ".disabled" new_path = dir_path os.rename(current_path, new_path) if is_disable: if os.path.exists(os.path.join(new_path, "disable.py")): disable_script = [sys.executable, "disable.py"] try_install_script(url, new_path, disable_script) else: if os.path.exists(os.path.join(new_path, "enable.py")): enable_script = [sys.executable, "enable.py"] try_install_script(url, new_path, enable_script) except Exception as e: print(f"{action_name}(git-clone) error: {url} / {e}", file=sys.stderr) return False print(f"{action_name} was successful.") return True def gitclone_update(files): import os print(f"Update: {files}") for url in files: if url.endswith("/"): url = url[:-1] try: repo_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") repo_path = os.path.join(custom_nodes_path, repo_name) git_pull(repo_path) if not execute_install_script(url, repo_path): return False except Exception as e: print(f"Update(git-clone) error: {url} / {e}", file=sys.stderr) return False print("Update was successful.") return True @server.PromptServer.instance.routes.post("/customnode/install") async def install_custom_node(request): json_data = await request.json() install_type = json_data['install_type'] print(f"Install custom node '{json_data['title']}'") res = False if len(json_data['files']) == 0: return web.Response(status=400) if install_type == "unzip": res = unzip_install(json_data['files']) if install_type == "copy": js_path_name = json_data['js_path'] if 'js_path' in json_data else '.' res = copy_install(json_data['files'], js_path_name) elif install_type == "git-clone": res = gitclone_install(json_data['files']) if 'pip' in json_data: for pname in json_data['pip']: install_cmd = [sys.executable, "-m", "pip", "install", pname] try_install_script(json_data['files'][0], ".", install_cmd) if res: print(f"After restarting ComfyUI, please refresh the browser.") return web.json_response({}, content_type='application/json') return web.Response(status=400) @server.PromptServer.instance.routes.post("/customnode/uninstall") async def uninstall_custom_node(request): json_data = await request.json() install_type = json_data['install_type'] print(f"Uninstall custom node '{json_data['title']}'") res = False if install_type == "copy": js_path_name = json_data['js_path'] if 'js_path' in json_data else '.' res = copy_uninstall(json_data['files'], js_path_name) elif install_type == "git-clone": res = gitclone_uninstall(json_data['files']) if res: print(f"After restarting ComfyUI, please refresh the browser.") return web.json_response({}, content_type='application/json') return web.Response(status=400) @server.PromptServer.instance.routes.post("/customnode/update") async def update_custom_node(request): json_data = await request.json() install_type = json_data['install_type'] print(f"Update custom node '{json_data['title']}'") res = False if install_type == "git-clone": res = gitclone_update(json_data['files']) if res: print(f"After restarting ComfyUI, please refresh the browser.") return web.json_response({}, content_type='application/json') return web.Response(status=400) @server.PromptServer.instance.routes.get("/comfyui_manager/update_comfyui") async def update_comfyui(request): print(f"Update ComfyUI") try: repo_path = os.path.dirname(folder_paths.__file__) if not os.path.exists(os.path.join(repo_path, '.git')): print(f"ComfyUI update fail: The installed ComfyUI does not have a Git repository.") return web.Response(status=400) # version check repo = git.Repo(repo_path) current_branch = repo.active_branch branch_name = current_branch.name remote_name = 'origin' remote = repo.remote(name=remote_name) remote.fetch() commit_hash = repo.head.commit.hexsha remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha if commit_hash != remote_commit_hash: git_pull(repo_path) execute_install_script("ComfyUI", repo_path) return web.Response(status=201) else: return web.Response(status=200) except Exception as e: print(f"ComfyUI update fail: {e}", file=sys.stderr) pass return web.Response(status=400) @server.PromptServer.instance.routes.post("/customnode/toggle_active") async def toggle_active(request): json_data = await request.json() install_type = json_data['install_type'] is_disabled = json_data['installed'] == "Disabled" print(f"Update custom node '{json_data['title']}'") res = False if install_type == "git-clone": res = gitclone_set_active(json_data['files'], not is_disabled) elif install_type == "copy": res = copy_set_active(json_data['files'], not is_disabled, json_data.get('js_path', None)) if res: return web.json_response({}, content_type='application/json') return web.Response(status=400) @server.PromptServer.instance.routes.post("/model/install") async def install_model(request): json_data = await request.json() model_path = get_model_path(json_data) res = False try: if model_path is not None: print(f"Install model '{json_data['name']}' into '{model_path}'") if json_data['url'].startswith('https://github.com') or json_data['url'].startswith('https://huggingface.co'): model_dir = get_model_dir(json_data) download_url(json_data['url'], model_dir) return web.json_response({}, content_type='application/json') else: res = download_url_with_agent(json_data['url'], model_path) else: print(f"Model installation error: invalid model type - {json_data['type']}") if res: return web.json_response({}, content_type='application/json') except Exception as e: print(f"[ERROR] {e}", file=sys.stderr) pass return web.Response(status=400) @server.PromptServer.instance.routes.get("/manager/preview_method") async def preview_method(request): if "value" in request.rel_url.query: set_preview_method(request.rel_url.query['value']) write_config() else: return web.Response(text=get_current_preview_method(), status=200) return web.Response(status=200) @server.PromptServer.instance.routes.get("/manager/badge_mode") async def badge_mode(request): if "value" in request.rel_url.query: set_badge_mode(request.rel_url.query['value']) write_config() else: return web.Response(text=get_config()['badge_mode'], status=200) return web.Response(status=200) @server.PromptServer.instance.routes.get("/manager/channel_url_list") async def channel_url_list(request): channels = default_channels+","+get_config()['channel_url_list'] channels = channels.split(',') if "value" in request.rel_url.query: for item in channels: name_url = item.split("::") if len(name_url) == 2 and name_url[0] == request.rel_url.query['value']: get_config()['channel_url'] = name_url[1] write_config() break else: selected = 'custom' selected_url = get_config()['channel_url'] for item in channels: item_info = item.split('::') if len(item_info) == 2 and item_info[1] == selected_url: selected = item_info[0] res = {'selected': selected, 'list': channels} return web.json_response(res, status=200) return web.Response(status=200) def get_matrix_auth(): if not os.path.exists(os.path.join(folder_paths.base_path, "matrix_auth")): return None with open(os.path.join(folder_paths.base_path, "matrix_auth"), "r") as f: matrix_auth = f.read() homeserver, username, password = matrix_auth.strip().split("\n") if not homeserver or not username or not password: return None return { "homeserver": homeserver, "username": username, "password": password, } def get_comfyworkflows_auth(): if not os.path.exists(os.path.join(folder_paths.base_path, "comfyworkflows_sharekey")): return None with open(os.path.join(folder_paths.base_path, "comfyworkflows_sharekey"), "r") as f: share_key = f.read() if not share_key.strip(): return None return share_key @server.PromptServer.instance.routes.get("/manager/get_matrix_auth") async def api_get_matrix_auth(request): # print("Getting stored Matrix credentials...") matrix_auth = get_matrix_auth() if not matrix_auth: return web.Response(status=404) return web.json_response(matrix_auth) @server.PromptServer.instance.routes.get("/manager/get_comfyworkflows_auth") async def api_get_comfyworkflows_auth(request): # Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken' # in the same directory as the ComfyUI base folder # print("Getting stored Comfyworkflows.com auth...") comfyworkflows_auth = get_comfyworkflows_auth() if not comfyworkflows_auth: return web.Response(status=404) return web.json_response({"comfyworkflows_sharekey" : comfyworkflows_auth}) def set_matrix_auth(json_data): homeserver = json_data['homeserver'] username = json_data['username'] password = json_data['password'] with open(os.path.join(folder_paths.base_path, "matrix_auth"), "w") as f: f.write("\n".join([homeserver, username, password])) def set_comfyworkflows_auth(comfyworkflows_sharekey): with open(os.path.join(folder_paths.base_path, "comfyworkflows_sharekey"), "w") as f: f.write(comfyworkflows_sharekey) def has_provided_matrix_auth(matrix_auth): return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip() def has_provided_comfyworkflows_auth(comfyworkflows_sharekey): return comfyworkflows_sharekey.strip() @server.PromptServer.instance.routes.post("/manager/share") async def share_art(request): # get json data json_data = await request.json() matrix_auth = json_data['matrix_auth'] comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey'] set_matrix_auth(matrix_auth) set_comfyworkflows_auth(comfyworkflows_sharekey) share_destinations = json_data['share_destinations'] credits = json_data['credits'] title = json_data['title'] description = json_data['description'] is_nsfw = json_data['is_nsfw'] prompt = json_data['prompt'] potential_outputs = json_data['potential_outputs'] # for now, pick the first output output_to_share = potential_outputs[0] assert output_to_share['type'] in ('image',) output_dir = folder_paths.get_output_directory() asset_filename = output_to_share['image']['filename'] asset_subfolder = output_to_share['image']['subfolder'] asset_output_type = output_to_share['image']['type'] assert asset_output_type == "output" if asset_subfolder: asset_filepath = os.path.join(output_dir, asset_subfolder, asset_filename) else: asset_filepath = os.path.join(output_dir, asset_filename) # get the mime type of the asset assetFileType = mimetypes.guess_type(asset_filepath)[0] if "comfyworkflows" in share_destinations: share_website_host = "https://comfyworkflows.com" share_endpoint = f"{share_website_host}/api" # get presigned urls async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: async with session.post( f"{share_endpoint}/get_presigned_urls", json={ "assetFileName": asset_filename, "assetFileType": assetFileType, }, ) as resp: assert resp.status == 200 presigned_urls_json = await resp.json() assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"] assetFileKey = presigned_urls_json["assetFileKey"] # upload asset async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp: assert resp.status == 200 # make a POST request to /api/upload_workflow with form data key values async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: form = aiohttp.FormData() if comfyworkflows_sharekey: form.add_field("shareKey", comfyworkflows_sharekey) form.add_field("source", "comfyui_manager") form.add_field("assetFileKey", assetFileKey) form.add_field("assetFileType", assetFileType) form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow'])) form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output'])) form.add_field("shareWorkflowCredits", credits) form.add_field("shareWorkflowTitle", title) form.add_field("shareWorkflowDescription", description) form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower()) async with session.post( f"{share_endpoint}/upload_workflow", data=form, ) as resp: assert resp.status == 200 upload_workflow_json = await resp.json() workflowId = upload_workflow_json["workflowId"] # check if the user has provided Matrix credentials if "matrix" in share_destinations: comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org' filename = os.path.basename(asset_filepath) content_type = assetFileType try: from matrix_client.api import MatrixHttpApi from matrix_client.client import MatrixClient homeserver = 'matrix.org' if matrix_auth: homeserver = matrix_auth.get('homeserver', 'matrix.org') homeserver = homeserver.replace("http://", "https://") if not homeserver.startswith("https://"): homeserver = "https://" + homeserver client = MatrixClient(homeserver) try: token = client.login(username=matrix_auth['username'], password=matrix_auth['password']) if not token: return web.json_response({"error" : "Invalid Matrix credentials."}, content_type='application/json', status=400) except: return web.json_response({"error" : "Invalid Matrix credentials."}, content_type='application/json', status=400) matrix = MatrixHttpApi(homeserver, token=token) with open(asset_filepath, 'rb') as f: mxc_url = matrix.media_upload(f.read(), content_type, filename=filename)['content_uri'] text_content = "" if title: text_content += f"{title}\n" if description: text_content += f"{description}\n" if credits: text_content += f"\ncredits: {credits}\n" response = matrix.send_message(comfyui_share_room_id, text_content) response = matrix.send_content(comfyui_share_room_id, mxc_url, filename, 'm.image') except: import traceback traceback.print_exc() return web.json_response({"error" : "An error occurred when sharing your art to Matrix."}, content_type='application/json', status=500) return web.json_response({ "comfyworkflows" : { "url" : None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}", }, "matrix" : { "success" : None if "matrix" not in share_destinations else True } }, content_type='application/json', status=200) WEB_DIRECTORY = "js" NODE_CLASS_MAPPINGS = {} __all__ = ['NODE_CLASS_MAPPINGS']