diff --git a/__init__.py b/__init__.py index fd8b291..ab85975 100644 --- a/__init__.py +++ b/__init__.py @@ -1,1702 +1,15 @@ -import mimetypes -import traceback - -import folder_paths -import locale -import subprocess # don't remove this -import concurrent -import nodes -import hashlib import os -import sys -import threading -import re -import shutil -from datetime import datetime -import git - -from server import PromptServer -from .glob import manager_core as core -import cm_global - -version = [2, 19] -version_str = f"V{version[0]}.{version[1]}" + (f'.{version[2]}' if len(version) > 2 else '') -print(f"### Loading: ComfyUI-Manager ({version_str})") - - -comfy_ui_hash = "-" - - -def handle_stream(stream, prefix): - stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') - for msg in stream: - if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg): - if msg.startswith('100%'): - print('\r' + msg, end="", file=sys.stderr), - else: - print('\r' + msg[:-1], end="", file=sys.stderr), - else: - if prefix == '[!]': - print(prefix, msg, end="", file=sys.stderr) - else: - print(prefix, msg, end="") - - -from comfy.cli_args import args -import latent_preview - - -class ManagerFuncsInComfyUI(core.ManagerFuncs): - def get_current_preview_method(self): - 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 run_script(self, cmd, cwd='.'): - if len(cmd) > 0 and cmd[0].startswith("#"): - print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`") - return 0 - - 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() - - -core.manager_funcs = ManagerFuncsInComfyUI() - - -sys.path.append('../..') - - -from torchvision.datasets.utils import download_url - - -comfy_path = os.path.dirname(folder_paths.__file__) -core.js_path = os.path.join(comfy_path, "web", "extensions") - -local_db_model = os.path.join(core.comfyui_manager_path, "model-list.json") -local_db_alter = os.path.join(core.comfyui_manager_path, "alter-list.json") -local_db_custom_node_list = os.path.join(core.comfyui_manager_path, "custom-node-list.json") -local_db_extension_node_mappings = os.path.join(core.comfyui_manager_path, "extension-node-map.json") -components_path = os.path.join(core.comfyui_manager_path, 'components') - - -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 - - core.get_config()['preview_method'] = args.preview_method - - -set_preview_method(core.get_config()['preview_method']) - - -def set_badge_mode(mode): - core.get_config()['badge_mode'] = mode - - -def set_default_ui_mode(mode): - core.get_config()['default_ui'] = mode - - -def set_component_policy(mode): - core.get_config()['component_policy'] = mode - - -def set_double_click_policy(mode): - core.get_config()['double_click_policy'] = mode - - -def print_comfyui_version(): - global comfy_ui_hash - - is_detached = False - try: - repo = git.Repo(os.path.dirname(folder_paths.__file__)) - core.comfy_ui_revision = len(list(repo.iter_commits('HEAD'))) - - comfy_ui_hash = repo.head.commit.hexsha - cm_global.variables['comfyui.revision'] = core.comfy_ui_revision - - core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime - cm_global.variables['comfyui.commit_datetime'] = core.comfy_ui_commit_datetime - - is_detached = repo.head.is_detached - current_branch = repo.active_branch.name - - try: - if core.comfy_ui_commit_datetime.date() < core.comfy_ui_required_commit_datetime.date(): - print(f"\n\n## [WARN] ComfyUI-Manager: Your ComfyUI version ({core.comfy_ui_revision})[{core.comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version. ##\n\n") - except: - pass - - # process on_revision_detected --> - if 'cm.on_revision_detected_handler' in cm_global.variables: - for k, f in cm_global.variables['cm.on_revision_detected_handler']: - try: - f(core.comfy_ui_revision) - except Exception: - print(f"[ERROR] '{k}' on_revision_detected_handler") - traceback.print_exc() - - del cm_global.variables['cm.on_revision_detected_handler'] - else: - print(f"[ComfyUI-Manager] Some features are restricted due to your ComfyUI being outdated.") - # <-- - - if current_branch == "master": - print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'") - else: - print(f"### ComfyUI Revision: {core.comfy_ui_revision} on '{current_branch}' [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'") - except: - if is_detached: - print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] *DETACHED | Released on '{core.comfy_ui_commit_datetime.date()}'") - else: - print("### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)") - - -print_comfyui_version() - - -async def populate_github_stats(json_obj, filename, silent=False): - uri = os.path.join(core.comfyui_manager_path, filename) - with open(uri, "r", encoding='utf-8') as f: - github_stats = json.load(f) - if 'custom_nodes' in json_obj: - for i, node in enumerate(json_obj['custom_nodes']): - url = node['reference'] - if url in github_stats: - json_obj['custom_nodes'][i]['stars'] = github_stats[url]['stars'] - json_obj['custom_nodes'][i]['last_update'] = github_stats[url]['last_update'] - else: - json_obj['custom_nodes'][i]['stars'] = -1 - json_obj['custom_nodes'][i]['last_update'] = -1 - return json_obj - - -def setup_environment(): - git_exe = core.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_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="") - - def process_custom_node(item): - core.check_a_custom_node_installed(item, do_fetch, do_update_check, do_update) - - with concurrent.futures.ThreadPoolExecutor(4) as executor: - for item in json_obj['custom_nodes']: - executor.submit(process_custom_node, item) - - 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.") - - -def nickname_filter(json_obj): - preemptions_map = {} - - for k, x in json_obj.items(): - if 'preemptions' in x[1]: - for y in x[1]['preemptions']: - preemptions_map[y] = k - elif k.endswith("/ComfyUI"): - for y in x[0]: - preemptions_map[y] = k - - updates = {} - for k, x in json_obj.items(): - removes = set() - for y in x[0]: - k2 = preemptions_map.get(y) - if k2 is not None and k != k2: - removes.add(y) - - if len(removes) > 0: - updates[k] = [y for y in x[0] if y not in removes] - - for k, v in updates.items(): - json_obj[k][0] = v - - return json_obj - - -@PromptServer.instance.routes.get("/customnode/getmappings") -async def fetch_customnode_mappings(request): - mode = request.rel_url.query["mode"] - - nickname_mode = False - if mode == "nickname": - mode = "local" - nickname_mode = True - - json_obj = await core.get_data_by_mode(mode, 'extension-node-map.json') - - if nickname_mode: - json_obj = nickname_filter(json_obj) - - all_nodes = set() - patterns = [] - for k, x in json_obj.items(): - all_nodes.update(set(x[0])) - - if 'nodename_pattern' in x[1]: - patterns.append((x[1]['nodename_pattern'], x[0])) - - missing_nodes = set(nodes.NODE_CLASS_MAPPINGS.keys()) - all_nodes - - for x in missing_nodes: - for pat, item in patterns: - if re.match(pat, x): - item.append(x) - - return web.json_response(json_obj, content_type='application/json') - - -@PromptServer.instance.routes.get("/customnode/fetch_updates") -async def fetch_updates(request): - try: - json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') - - 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) - - -@PromptServer.instance.routes.get("/customnode/update_all") -async def update_all(request): - try: - save_snapshot_with_postfix('autosave') - - json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') - - check_custom_nodes_installed(json_obj, do_update=True) - - updated = [item['title'] for item in json_obj['custom_nodes'] if item['installed'] == 'Update'] - failed = [item['title'] for item in json_obj['custom_nodes'] if item['installed'] == 'Fail'] - - res = {'updated': updated, 'failed': failed} - - if len(updated) == 0 and len(failed) == 0: - status = 200 - else: - status = 201 - - return web.json_response(res, status=status, content_type='application/json') - except: - return web.Response(status=400) - finally: - core.clear_pip_cache() - - -def convert_markdown_to_html(input_text): - pattern_a = re.compile(r'\[a/([^]]+)\]\(([^)]+)\)') - pattern_w = re.compile(r'\[w/([^]]+)\]') - pattern_i = re.compile(r'\[i/([^]]+)\]') - pattern_bold = re.compile(r'\*\*([^*]+)\*\*') - pattern_white = re.compile(r'%%([^*]+)%%') - - def replace_a(match): - return f"{match.group(1)}" - - def replace_w(match): - return f"

{match.group(1)}

" - - def replace_i(match): - return f"

{match.group(1)}

" - - def replace_bold(match): - return f"{match.group(1)}" - - def replace_white(match): - return f"{match.group(1)}" - - input_text = input_text.replace('\\[', '[').replace('\\]', ']').replace('<', '<').replace('>', '>') - - result_text = re.sub(pattern_a, replace_a, input_text) - result_text = re.sub(pattern_w, replace_w, result_text) - result_text = re.sub(pattern_i, replace_i, result_text) - result_text = re.sub(pattern_bold, replace_bold, result_text) - result_text = re.sub(pattern_white, replace_white, result_text) - - return result_text.replace("\n", "
") - - -def populate_markdown(x): - if 'description' in x: - x['description'] = convert_markdown_to_html(x['description']) - - if 'name' in x: - x['name'] = x['name'].replace('<', '<').replace('>', '>') - - if 'title' in x: - x['title'] = x['title'].replace('<', '<').replace('>', '>') - - -@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": - channel = 'local' - else: - channel = core.get_config()['channel_url'] - - json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') - json_obj = await populate_github_stats(json_obj, "github-stats.json") - - def is_ignored_notice(code): - global version - - if code is not None and code.startswith('#NOTICE_'): - try: - notice_version = [int(x) for x in code[8:].split('.')] - return notice_version[0] < version[0] or (notice_version[0] == version[0] and notice_version[1] <= version[1]) - except Exception: - return False - else: - return False - - json_obj['custom_nodes'] = [record for record in json_obj['custom_nodes'] if not is_ignored_notice(record.get('author'))] - - check_custom_nodes_installed(json_obj, False, not skip_update) - - for x in json_obj['custom_nodes']: - populate_markdown(x) - - if channel != 'local': - found = 'custom' - - for name, url in core.get_channel_dict().items(): - if url == channel: - found = name - break - - channel = found - - json_obj['channel'] = channel - - return web.json_response(json_obj, content_type='application/json') - - -@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 - - alter_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json') - custom_node_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') - - 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] - core.check_a_custom_node_installed(custom_node, not skip_update) - - populate_markdown(item) - populate_markdown(custom_node) - item['custom_node'] = custom_node - - return web.json_response(alter_json, content_type='application/json') - - -def check_model_installed(json_obj): - def process_model(item): - model_path = get_model_path(item) - item['installed'] = 'None' - - if model_path is not None: - if os.path.exists(model_path): - item['installed'] = 'True' - else: - item['installed'] = 'False' - - with concurrent.futures.ThreadPoolExecutor(8) as executor: - for item in json_obj['models']: - executor.submit(process_model, item) - - -@PromptServer.instance.routes.get("/externalmodel/getlist") -async def fetch_externalmodel_list(request): - json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'model-list.json') - - check_model_installed(json_obj) - - for x in json_obj['models']: - populate_markdown(x) - - return web.json_response(json_obj, content_type='application/json') - - -@PromptServer.instance.routes.get("/snapshot/get_current") -async def get_snapshot_list(request): - json_obj = get_current_snapshot() - return web.json_response(json_obj, content_type='application/json') - - -@PromptServer.instance.routes.get("/snapshot/getlist") -async def get_snapshot_list(request): - snapshots_directory = os.path.join(os.path.dirname(__file__), 'snapshots') - items = [f[:-5] for f in os.listdir(snapshots_directory) if f.endswith('.json')] - items.sort(reverse=True) - return web.json_response({'items': items}, content_type='application/json') - - -@PromptServer.instance.routes.get("/snapshot/remove") -async def remove_snapshot(request): - try: - target = request.rel_url.query["target"] - - path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json") - if os.path.exists(path): - os.remove(path) - - return web.Response(status=200) - except: - return web.Response(status=400) - - -@PromptServer.instance.routes.get("/snapshot/restore") -async def remove_snapshot(request): - try: - target = request.rel_url.query["target"] - - path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}.json") - if os.path.exists(path): - if not os.path.exists(core.startup_script_path): - os.makedirs(core.startup_script_path) - - target_path = os.path.join(core.startup_script_path, "restore-snapshot.json") - shutil.copy(path, target_path) - - print(f"Snapshot restore scheduled: `{target}`") - return web.Response(status=200) - - print(f"Snapshot file not found: `{path}`") - return web.Response(status=400) - except: - return web.Response(status=400) - - -def get_current_snapshot(): - # Get ComfyUI hash - 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) - - repo = git.Repo(repo_path) - comfyui_commit_hash = repo.head.commit.hexsha - - git_custom_nodes = {} - file_custom_nodes = [] - - # Get custom nodes hash - for path in os.listdir(core.custom_nodes_path): - fullpath = os.path.join(core.custom_nodes_path, path) - - if os.path.isdir(fullpath): - is_disabled = path.endswith(".disabled") - - try: - git_dir = os.path.join(fullpath, '.git') - - if not os.path.exists(git_dir): - continue - - repo = git.Repo(fullpath) - commit_hash = repo.head.commit.hexsha - url = repo.remotes.origin.url - git_custom_nodes[url] = { - 'hash': commit_hash, - 'disabled': is_disabled - } - - except: - print(f"Failed to extract snapshots for the custom node '{path}'.") - - elif path.endswith('.py'): - is_disabled = path.endswith(".py.disabled") - filename = os.path.basename(path) - item = { - 'filename': filename, - 'disabled': is_disabled - } - - file_custom_nodes.append(item) - - return { - 'comfyui': comfyui_commit_hash, - 'git_custom_nodes': git_custom_nodes, - 'file_custom_nodes': file_custom_nodes, - } - - -def save_snapshot_with_postfix(postfix): - now = datetime.now() - - date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S") - file_name = f"{date_time_format}_{postfix}" - - path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{file_name}.json") - with open(path, "w") as json_file: - json.dump(get_current_snapshot(), json_file, indent=4) - - -@PromptServer.instance.routes.get("/snapshot/get_current") -async def get_current_snapshot_api(request): - try: - return web.json_response(get_current_snapshot(), content_type='application/json') - except: - return web.Response(status=400) - - -@PromptServer.instance.routes.get("/snapshot/save") -async def save_snapshot(request): - try: - save_snapshot_with_postfix('snapshot') - return web.Response(status=200) - except: - return web.Response(status=400) - - -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(core.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, core.custom_nodes_path) - else: - path = os.path.join(core.js_path, js_path_name) if js_path_name is not None else core.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 = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.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 = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.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 - - -@PromptServer.instance.routes.post("/customnode/install") -async def install_custom_node(request): - json_data = await request.json() - install_type = json_data['install_type'] +cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode') - print(f"Install custom node '{json_data['title']}'") +if not os.path.exists(cli_mode_flag): + from .glob import manager_server + WEB_DIRECTORY = "js" +else: + print(f"\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n") - 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 = core.gitclone_install(json_data['files']) - - if 'pip' in json_data: - for pname in json_data['pip']: - pkg = core.remap_pip_package(pname) - install_cmd = [sys.executable, "-m", "pip", "install", pkg] - core.try_install_script(json_data['files'][0], ".", install_cmd) - - core.clear_pip_cache() - - if res: - print(f"After restarting ComfyUI, please refresh the browser.") - return web.json_response({}, content_type='application/json') - - return web.Response(status=400) - - -@PromptServer.instance.routes.post("/customnode/fix") -async def fix_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 == "git-clone": - res = core.gitclone_fix(json_data['files']) - else: - return web.Response(status=400) - - if 'pip' in json_data: - for pname in json_data['pip']: - install_cmd = [sys.executable, "-m", "pip", "install", '-U', pname] - core.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) - - -@PromptServer.instance.routes.get("/customnode/install/git_url") -async def install_custom_node_git_url(request): - res = False - if "url" in request.rel_url.query: - url = request.rel_url.query['url'] - res = core.gitclone_install([url]) - - if res: - print(f"After restarting ComfyUI, please refresh the browser.") - return web.Response(status=200) - - return web.Response(status=400) - - -@PromptServer.instance.routes.get("/customnode/install/pip") -async def install_custom_node_git_url(request): - res = False - if "packages" in request.rel_url.query: - packages = request.rel_url.query['packages'] - core.pip_install(packages.split(' ')) - - return web.Response(status=200) - - -@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 = core.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) - - -@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 = core.gitclone_update(json_data['files']) - - core.clear_pip_cache() - - if res: - print(f"After restarting ComfyUI, please refresh the browser.") - return web.json_response({}, content_type='application/json') - - return web.Response(status=400) - - -@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) - - if repo.head.is_detached: - core.switch_to_default_branch(repo) - - current_branch = repo.active_branch - branch_name = current_branch.name - - if current_branch.tracking_branch() is None: - print(f"[ComfyUI-Manager] There is no tracking branch ({current_branch})") - remote_name = 'origin' - else: - remote_name = current_branch.tracking_branch().remote_name - remote = repo.remote(name=remote_name) - - try: - remote.fetch() - except Exception as e: - if 'detected dubious' in str(e): - print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on 'ComfyUI' repository") - safedir_path = comfy_path.replace('\\', '/') - subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', safedir_path]) - try: - remote.fetch() - except Exception: - print(f"\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n" - f"-----------------------------------------------------------------------------------------\n" - f'git config --global --add safe.directory "{safedir_path}"\n' - f"-----------------------------------------------------------------------------------------\n") - - commit_hash = repo.head.commit.hexsha - remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha - - if commit_hash != remote_commit_hash: - core.git_pull(repo_path) - core.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) - - -@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 = core.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) - - -@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}'") - - model_url = json_data['url'] - if not core.get_config()['model_download_by_agent'] and (model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')): - model_dir = get_model_dir(json_data) - download_url(model_url, model_dir, filename=json_data['filename']) - - return web.json_response({}, content_type='application/json') - else: - res = download_url_with_agent(model_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) - - -class ManagerTerminalHook: - def write_stderr(self, msg): - PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg}) - - def write_stdout(self, msg): - PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg}) - - -manager_terminal_hook = ManagerTerminalHook() - - -@PromptServer.instance.routes.get("/manager/terminal") -async def terminal_mode(request): - if "mode" in request.rel_url.query: - if request.rel_url.query['mode'] == 'true': - sys.__comfyui_manager_terminal_hook.add_hook('cm', manager_terminal_hook) - else: - sys.__comfyui_manager_terminal_hook.remove_hook('cm') - - return web.Response(status=200) - - -@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']) - core.write_config() - else: - return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200) - - return web.Response(status=200) - - -@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']) - core.write_config() - else: - return web.Response(text=core.get_config()['badge_mode'], status=200) - - return web.Response(status=200) - - -@PromptServer.instance.routes.get("/manager/default_ui") -async def default_ui_mode(request): - if "value" in request.rel_url.query: - set_default_ui_mode(request.rel_url.query['value']) - core.write_config() - else: - return web.Response(text=core.get_config()['default_ui'], status=200) - - return web.Response(status=200) - - -@PromptServer.instance.routes.get("/manager/component/policy") -async def component_policy(request): - if "value" in request.rel_url.query: - set_component_policy(request.rel_url.query['value']) - core.write_config() - else: - return web.Response(text=core.get_config()['component_policy'], status=200) - - return web.Response(status=200) - - -@PromptServer.instance.routes.get("/manager/dbl_click/policy") -async def dbl_click_policy(request): - if "value" in request.rel_url.query: - set_double_click_policy(request.rel_url.query['value']) - core.write_config() - else: - return web.Response(text=core.get_config()['double_click_policy'], status=200) - - return web.Response(status=200) - - -@PromptServer.instance.routes.get("/manager/channel_url_list") -async def channel_url_list(request): - channels = core.get_channel_dict() - if "value" in request.rel_url.query: - channel_url = channels.get(request.rel_url.query['value']) - if channel_url is not None: - core.get_config()['channel_url'] = channel_url - core.write_config() - else: - selected = 'custom' - selected_url = core.get_config()['channel_url'] - - for name, url in channels.items(): - if url == selected_url: - selected = name - break - - res = {'selected': selected, - 'list': core.get_channel_list()} - return web.json_response(res, status=200) - - return web.Response(status=200) - - -@PromptServer.instance.routes.get("/manager/notice") -async def get_notice(request): - url = "github.com" - path = "/ltdrdata/ltdrdata.github.io/wiki/News" - - async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: - async with session.get(f"https://{url}{path}") as response: - if response.status == 200: - # html_content = response.read().decode('utf-8') - html_content = await response.text() - - pattern = re.compile(r'
([\s\S]*?)
') - match = pattern.search(html_content) - - if match: - markdown_content = match.group(1) - markdown_content += f"
ComfyUI: {core.comfy_ui_revision}[{comfy_ui_hash[:6]}]({core.comfy_ui_commit_datetime.date()})" - # markdown_content += f"
         ()" - markdown_content += f"
Manager: {version_str}" - - try: - if core.comfy_ui_required_commit_datetime.date() > core.comfy_ui_commit_datetime.date(): - markdown_content = f'

Your ComfyUI is too OUTDATED!!!

' + markdown_content - except: - pass - - return web.Response(text=markdown_content, status=200) - else: - return web.Response(text="Unable to retrieve Notice", status=200) - else: - return web.Response(text="Unable to retrieve Notice", status=200) - - -@PromptServer.instance.routes.get("/manager/reboot") -def restart(self): - try: - sys.stdout.close_log() - except Exception as e: - pass - - return os.execv(sys.executable, [sys.executable] + sys.argv) - - -def sanitize_filename(input_string): - # 알파벳, 숫자, 및 밑줄 이외의 문자를 밑줄로 대체 - result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string) - return result_string - - -@PromptServer.instance.routes.post("/manager/component/save") -async def save_component(request): - try: - data = await request.json() - name = data['name'] - workflow = data['workflow'] - - if not os.path.exists(components_path): - os.mkdir(components_path) - - if 'packname' in workflow and workflow['packname'] != '': - sanitized_name = sanitize_filename(workflow['packname'])+'.pack' - else: - sanitized_name = sanitize_filename(name)+'.json' - - filepath = os.path.join(components_path, sanitized_name) - components = {} - if os.path.exists(filepath): - with open(filepath) as f: - components = json.load(f) - - components[name] = workflow - - with open(filepath, 'w') as f: - json.dump(components, f, indent=4, sort_keys=True) - return web.Response(text=filepath, status=200) - except: - return web.Response(status=400) - - -@PromptServer.instance.routes.post("/manager/component/loads") -async def load_components(request): - try: - json_files = [f for f in os.listdir(components_path) if f.endswith('.json')] - pack_files = [f for f in os.listdir(components_path) if f.endswith('.pack')] - - components = {} - for json_file in json_files + pack_files: - file_path = os.path.join(components_path, json_file) - with open(file_path, 'r') as file: - try: - # When there is a conflict between the .pack and the .json, the pack takes precedence and overrides. - components.update(json.load(file)) - except json.JSONDecodeError as e: - print(f"[ComfyUI-Manager] Error decoding component file in file {json_file}: {e}") - - return web.json_response(components) - except Exception as e: - print(f"[ComfyUI-Manager] failed to load components\n{e}") - return web.Response(status=400) - - -@PromptServer.instance.routes.get("/manager/share_option") -async def share_option(request): - if "value" in request.rel_url.query: - core.get_config()['share_option'] = request.rel_url.query['value'] - core.write_config() - else: - return web.Response(text=core.get_config()['share_option'], status=200) - - return web.Response(status=200) - - -def get_openart_auth(): - if not os.path.exists(os.path.join(core.comfyui_manager_path, ".openart_key")): - return None - try: - with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "r") as f: - openart_key = f.read().strip() - return openart_key if openart_key else None - except: - return None - - -def get_matrix_auth(): - if not os.path.exists(os.path.join(core.comfyui_manager_path, "matrix_auth")): - return None - try: - with open(os.path.join(core.comfyui_manager_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, - } - except: - return None - - -def get_comfyworkflows_auth(): - if not os.path.exists(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey")): - return None - try: - with open(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey"), "r") as f: - share_key = f.read() - if not share_key.strip(): - return None - return share_key - except: - return None - - -def get_youml_settings(): - if not os.path.exists(os.path.join(core.comfyui_manager_path, ".youml")): - return None - try: - with open(os.path.join(core.comfyui_manager_path, ".youml"), "r") as f: - youml_settings = f.read().strip() - return youml_settings if youml_settings else None - except: - return None - - -def set_youml_settings(settings): - with open(os.path.join(core.comfyui_manager_path, ".youml"), "w") as f: - f.write(settings) - - -@PromptServer.instance.routes.get("/manager/get_openart_auth") -async def api_get_openart_auth(request): - # print("Getting stored Matrix credentials...") - openart_key = get_openart_auth() - if not openart_key: - return web.Response(status=404) - return web.json_response({"openart_key": openart_key}) - - -@PromptServer.instance.routes.post("/manager/set_openart_auth") -async def api_set_openart_auth(request): - json_data = await request.json() - openart_key = json_data['openart_key'] - with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "w") as f: - f.write(openart_key) - return web.Response(status=200) - - -@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) - - -@PromptServer.instance.routes.get("/manager/youml/settings") -async def api_get_youml_settings(request): - youml_settings = get_youml_settings() - if not youml_settings: - return web.Response(status=404) - return web.json_response(json.loads(youml_settings)) - - -@PromptServer.instance.routes.post("/manager/youml/settings") -async def api_set_youml_settings(request): - json_data = await request.json() - set_youml_settings(json.dumps(json_data)) - return web.Response(status=200) - - -@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}) - -args.enable_cors_header = "*" -if hasattr(PromptServer.instance, "app"): - app = PromptServer.instance.app - cors_middleware = server.create_cors_middleware(args.enable_cors_header) - app.middlewares.append(cors_middleware) - - -@PromptServer.instance.routes.post("/manager/set_esheep_workflow_and_images") -async def set_esheep_workflow_and_images(request): - json_data = await request.json() - current_workflow = json_data['workflow'] - images = json_data['images'] - with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), "w", encoding='utf-8') as file: - json.dump(json_data, file, indent=4) - return web.Response(status=200) - - -@PromptServer.instance.routes.get("/manager/get_esheep_workflow_and_images") -async def get_esheep_workflow_and_images(request): - with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file: - data = json.load(file) - return web.Response(status=200, text=json.dumps(data)) - - -def set_matrix_auth(json_data): - homeserver = json_data['homeserver'] - username = json_data['username'] - password = json_data['password'] - with open(os.path.join(core.comfyui_manager_path, "matrix_auth"), "w") as f: - f.write("\n".join([homeserver, username, password])) - - -def set_comfyworkflows_auth(comfyworkflows_sharekey): - with open(os.path.join(core.comfyui_manager_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() - - -def extract_model_file_names(json_data): - """Extract unique file names from the input JSON data.""" - file_names = set() - model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'} - - # Recursively search for file names in the JSON data - def recursive_search(data): - if isinstance(data, dict): - for value in data.values(): - recursive_search(value) - elif isinstance(data, list): - for item in data: - recursive_search(item) - elif isinstance(data, str) and '.' in data: - file_names.add(os.path.basename(data)) # file_names.add(data) - - recursive_search(json_data) - return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions] - - -def find_file_paths(base_dir, file_names): - """Find the paths of the files in the base directory.""" - file_paths = {} - - for root, dirs, files in os.walk(base_dir): - # Exclude certain directories - dirs[:] = [d for d in dirs if d not in ['.git']] - - for file in files: - if file in file_names: - file_paths[file] = os.path.join(root, file) - return file_paths - - -def compute_sha256_checksum(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() - - -@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'] - selected_output_index = json_data['selected_output_index'] - - try: - output_to_share = potential_outputs[int(selected_output_index)] - except: - # for now, pick the first output - output_to_share = potential_outputs[0] - - assert output_to_share['type'] in ('image', 'output') - output_dir = folder_paths.get_output_directory() - - if output_to_share['type'] == 'image': - asset_filename = output_to_share['image']['filename'] - asset_subfolder = output_to_share['image']['subfolder'] - - if output_to_share['image']['type'] == 'temp': - output_dir = folder_paths.get_temp_directory() - else: - asset_filename = output_to_share['output']['filename'] - asset_subfolder = output_to_share['output']['subfolder'] - - 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] - - share_website_host = "UNKNOWN" - 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, - "workflowJsonFileName" : 'workflow.json', - "workflowJsonFileType" : 'application/json', - }, - ) as resp: - assert resp.status == 200 - presigned_urls_json = await resp.json() - assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"] - assetFileKey = presigned_urls_json["assetFileKey"] - workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"] - workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"] - - # 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 - - # upload workflow json - async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: - async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp: - assert resp.status == 200 - - model_filenames = extract_model_file_names(prompt['workflow']) - model_file_paths = find_file_paths(folder_paths.base_path, model_filenames) - - models_info = {} - for filename, filepath in model_file_paths.items(): - models_info[filename] = { - "filename": filename, - "sha256_checksum": compute_sha256_checksum(filepath), - "relative_path": os.path.relpath(filepath, folder_paths.base_path), - } - - # 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("workflowJsonFileKey", workflowJsonFileKey) - 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()) - form.add_field("currentSnapshot", json.dumps(get_current_snapshot())) - form.add_field("modelsInfo", json.dumps(models_info)) - - 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'] - - workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['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') - response = matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file') - 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) - - -def sanitize(data): - return data.replace("<", "<").replace(">", ">") - - -async def _confirm_try_install(sender, custom_node_url, msg): - json_obj = await core.get_data_by_mode('default', 'custom-node-list.json') - - sender = sanitize(sender) - msg = sanitize(msg) - target = core.lookup_customnode_by_url(json_obj, custom_node_url) - - if target is not None: - PromptServer.instance.send_sync("cm-api-try-install-customnode", - {"sender": sender, "target": target, "msg": msg}) - else: - print(f"[ComfyUI Manager API] Failed to try install - Unknown custom node url '{custom_node_url}'") - - -def confirm_try_install(sender, custom_node_url, msg): - asyncio.run(_confirm_try_install(sender, custom_node_url, msg)) - - -cm_global.register_api('cm.try-install-custom-node', confirm_try_install) - - -import asyncio - - -async def default_cache_update(): - async def get_cache(filename): - uri = 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/' + filename - cache_uri = str(core.simple_hash(uri)) + '_' + filename - cache_uri = os.path.join(core.cache_dir, cache_uri) - - json_obj = await core.get_data(uri, True) - - with core.cache_lock: - with open(cache_uri, "w", encoding='utf-8') as file: - json.dump(json_obj, file, indent=4, sort_keys=True) - print(f"[ComfyUI-Manager] default cache updated: {uri}") - - a = get_cache("custom-node-list.json") - b = get_cache("extension-node-map.json") - c = get_cache("model-list.json") - d = get_cache("alter-list.json") - - await asyncio.gather(a, b, c, d) - - -threading.Thread(target=lambda: asyncio.run(default_cache_update())).start() - - -if not os.path.exists(core.config_path): - core.get_config() - core.write_config() - - -WEB_DIRECTORY = "js" NODE_CLASS_MAPPINGS = {} __all__ = ['NODE_CLASS_MAPPINGS'] -cm_global.register_extension('ComfyUI-Manager', - {'version': version, - 'name': 'ComfyUI Manager', - 'nodes': {'Terminal Log //CM'}, - 'description': 'It provides the ability to manage custom nodes in ComfyUI.', }) + diff --git a/cm-cli.py b/cm-cli.py new file mode 100644 index 0000000..2015317 --- /dev/null +++ b/cm-cli.py @@ -0,0 +1,465 @@ + +import os +import sys +import traceback +import json +import asyncio +import subprocess + +sys.path.append("./glob") +import manager_core as core +import cm_global +import git + + +print(f"\n-= ComfyUI-Manager CLI ({core.version_str}) =-\n") + +comfyui_manager_path = os.path.dirname(__file__) +comfy_path = os.environ.get('COMFYUI_PATH') + +if comfy_path is None: + print(f"WARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.\n", file=sys.stderr) + comfy_path = os.path.abspath(os.path.join(comfyui_manager_path, '..', '..')) + +startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts") +custom_nodes_path = os.path.join(comfy_path, 'custom_nodes') + +script_path = os.path.join(startup_script_path, "install-scripts.txt") +restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json") +pip_overrides_path = os.path.join(comfyui_manager_path, "pip_overrides.json") +git_script_path = os.path.join(comfyui_manager_path, "git_helper.py") + +cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia'] +cm_global.pip_overrides = {} +if os.path.exists(pip_overrides_path): + with open(pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file: + cm_global.pip_overrides = json.load(json_file) + + +processed_install = set() + + +def post_install(url): + try: + repository_name = url.split("/")[-1].strip() + repo_path = os.path.join(custom_nodes_path, repository_name) + repo_path = os.path.abspath(repo_path) + + requirements_path = os.path.join(repo_path, 'requirements.txt') + install_script_path = os.path.join(repo_path, 'install.py') + + if os.path.exists(requirements_path): + with (open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file): + for line in file: + package_name = core.remap_pip_package(line.strip()) + if package_name and not core.is_installed(package_name): + install_cmd = [sys.executable, "-m", "pip", "install", package_name] + output = subprocess.check_output(install_cmd, cwd=repo_path, text=True) + for msg_line in output.split('\n'): + if 'Requirement already satisfied:' in msg_line: + print('.', end='') + else: + print(msg_line) + + if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install: + processed_install.add(f'{repo_path}/install.py') + install_cmd = [sys.executable, install_script_path] + output = subprocess.check_output(install_cmd, cwd=repo_path, text=True) + for msg_line in output.split('\n'): + if 'Requirement already satisfied:' in msg_line: + print('.', end='') + else: + print(msg_line) + + except Exception: + print(f"ERROR: Restoring '{url}' is failed.") + + +def restore_snapshot(snapshot_name): + global processed_install + + snapshot_path = os.path.join(core.comfyui_manager_path, 'snapshots', snapshot_name) + if not os.path.exists(snapshot_path): + print(f"ERROR: `{snapshot_path}` is not exists.") + exit(-1) + + try: + cloned_repos = [] + checkout_repos = [] + skipped_repos = [] + enabled_repos = [] + disabled_repos = [] + is_failed = False + + def extract_infos(msg_lines): + nonlocal is_failed + + for x in msg_lines: + if x.startswith("CLONE: "): + cloned_repos.append(x[7:]) + elif x.startswith("CHECKOUT: "): + checkout_repos.append(x[10:]) + elif x.startswith("SKIPPED: "): + skipped_repos.append(x[9:]) + elif x.startswith("ENABLE: "): + enabled_repos.append(x[8:]) + elif x.startswith("DISABLE: "): + disabled_repos.append(x[9:]) + elif 'APPLY SNAPSHOT: False' in x: + is_failed = True + + print(f"Restore snapshot.") + cmd_str = [sys.executable, git_script_path, '--apply-snapshot', snapshot_path] + output = subprocess.check_output(cmd_str, cwd=custom_nodes_path, text=True) + msg_lines = output.split('\n') + extract_infos(msg_lines) + + for url in cloned_repos: + post_install(url) + + # print summary + for x in cloned_repos: + print(f"[ INSTALLED ] {x}") + for x in checkout_repos: + print(f"[ CHECKOUT ] {x}") + for x in enabled_repos: + print(f"[ ENABLED ] {x}") + for x in disabled_repos: + print(f"[ DISABLED ] {x}") + + if is_failed: + print("ERROR: Failed to restore snapshot.") + + except Exception: + print("ERROR: Failed to restore snapshot.") + traceback.print_exc() + exit(-1) + + +def check_comfyui_hash(): + repo = git.Repo(comfy_path) + core.comfy_ui_revision = len(list(repo.iter_commits('HEAD'))) + + comfy_ui_hash = repo.head.commit.hexsha + cm_global.variables['comfyui.revision'] = core.comfy_ui_revision + + core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime + + +check_comfyui_hash() + + +def read_downgrade_blacklist(): + try: + import configparser + config_path = os.path.join(os.path.dirname(__file__), "config.ini") + config = configparser.ConfigParser() + config.read(config_path) + default_conf = config['default'] + + if 'downgrade_blacklist' in default_conf: + items = default_conf['downgrade_blacklist'].split(',') + items = [x.strip() for x in items if x != ''] + cm_global.pip_downgrade_blacklist += items + cm_global.pip_downgrade_blacklist = list(set(cm_global.pip_downgrade_blacklist)) + except: + pass + + +read_downgrade_blacklist() + + +if not (len(sys.argv) == 2 and sys.argv[1] == 'save-snapshot') and len(sys.argv) < 3: + print(f"\npython cm-cli.py [OPTIONS]\n\n" + f"OPTIONS:\n" + f" [install|uninstall|update|disable|enable|fix] node_name ... ?[--channel ] ?[--mode [remote|local|cache]]\n" + f" [update|disable|enable|fix] all ?[--channel ] ?[--mode [remote|local|cache]]\n" + f" [simple-show|show] [installed|enabled|not-installed|disabled|all|snapshot|snapshot-list] ?[--channel ] ?[--mode [remote|local|cache]]\n" + f" save-snapshot\n" + f" restore-snapshot \n" + f" cli-only-mode [enable|disable]\n" + f" clear\n") + exit(-1) + + +channel = 'default' +mode = 'remote' +nodes = set() + + +def load_custom_nodes(): + channel_dict = core.get_channel_dict() + if channel not in channel_dict: + print(f"ERROR: Invalid channel is specified `--channel {channel}`", file=sys.stderr) + exit(-1) + + if mode not in ['remote', 'local', 'cache']: + print(f"ERROR: Invalid mode is specified `--mode {mode}`", file=sys.stderr) + exit(-1) + + channel_url = channel_dict[channel] + + res = {} + json_obj = asyncio.run(core.get_data_by_mode(mode, 'custom-node-list.json', channel_url=channel_url)) + for x in json_obj['custom_nodes']: + for y in x['files']: + if 'github.com' in y and not (y.endswith('.py') or y.endswith('.js')): + repo_name = y.split('/')[-1] + res[repo_name] = x + + return res + + +def process_args(): + global channel + global mode + + i = 2 + while i < len(sys.argv): + if sys.argv[i] == '--channel': + if i+1 < len(sys.argv): + channel = sys.argv[i+1] + i += 1 + elif sys.argv[i] == '--mode': + if i+1 < len(sys.argv): + mode = sys.argv[i+1] + i += 1 + else: + nodes.add(sys.argv[i]) + + i += 1 + + +process_args() +custom_node_map = load_custom_nodes() + + +def lookup_node_path(node_name): + # Currently, the node_name is used directly as the node_path, but in the future, I plan to allow nicknames. + if node_name in custom_node_map: + node_path = os.path.join(custom_nodes_path, node_name) + return node_path, custom_node_map[node_name] + + print(f"ERROR: invalid node name '{node_name}'") + exit(-1) + + +def install_node(node_name, is_all=False): + node_path, node_item = lookup_node_path(node_name) + + if os.path.exists(node_path): + if not is_all: + print(f"[ SKIPPED ] {node_name:50} => Already installed") + elif os.path.exists(node_path+'.disabled'): + enable_node(node_name) + else: + res = core.gitclone_install(node_item['files'], instant_execution=True) + if not res: + print(f"ERROR: An error occurred while installing '{node_name}'.") + else: + print(f"[INSTALLED] {node_name:50}") + + +def fix_node(node_name, is_all=False): + node_path, node_item = lookup_node_path(node_name) + if os.path.exists(node_path): + res = core.gitclone_fix(node_item['files'], instant_execution=True) + if not res: + print(f"ERROR: An error occurred while fixing '{node_name}'.") + elif not is_all and os.path.exists(node_path+'.disabled'): + print(f"[ SKIPPED ]: {node_name:50} => Disabled") + elif not is_all: + print(f"[ SKIPPED ]: {node_name:50} => Not installed") + + +def uninstall_node(node_name, is_all=False): + node_path, node_item = lookup_node_path(node_name) + if os.path.exists(node_path) or os.path.exists(node_path+'.disabled'): + res = core.gitclone_uninstall(node_item['files']) + if not res: + print(f"ERROR: An error occurred while uninstalling '{node_name}'.") + else: + print(f"[UNINSTALLED] {node_name:50}") + else: + print(f"[ SKIPPED ]: {node_name:50} => Not installed") + + +def update_node(node_name, is_all=False): + node_path, node_item = lookup_node_path(node_name) + res = core.gitclone_update(node_item['files'], skip_script=True) + post_install(node_path) + if not res: + print(f"ERROR: An error occurred while uninstalling '{node_name}'.") + + +def enable_node(node_name, is_all=False): + if node_name == 'ComfyUI-Manager': + return + + node_path, _ = lookup_node_path(node_name) + + if os.path.exists(node_path+'.disabled'): + current_name = node_path+'.disabled' + os.rename(current_name, node_path) + print(f"[ENABLED] {node_name:50}") + elif os.path.exists(node_path): + print(f"[SKIPPED] {node_name:50} => Already enabled") + elif not is_all: + print(f"[SKIPPED] {node_name:50} => Not installed") + + +def disable_node(node_name, is_all=False): + if node_name == 'ComfyUI-Manager': + return + + node_path, _ = lookup_node_path(node_name) + + if os.path.exists(node_path): + current_name = node_path + new_name = node_path+'.disabled' + os.rename(current_name, new_name) + print(f"[DISABLED] {node_name:50}") + elif os.path.exists(node_path+'.disabled'): + print(f"[ SKIPPED] {node_name:50} => Already disabled") + elif not is_all: + print(f"[ SKIPPED] {node_name:50} => Not installed") + + +def show_list(kind, simple=False): + for k, v in custom_node_map.items(): + node_path = os.path.join(custom_nodes_path, k) + + states = set() + if os.path.exists(node_path): + prefix = '[ ENABLED ] ' + states.add('installed') + states.add('enabled') + states.add('all') + elif os.path.exists(node_path+'.disabled'): + prefix = '[ DISABLED ] ' + states.add('installed') + states.add('disabled') + states.add('all') + else: + prefix = '[ NOT INSTALLED ] ' + states.add('not-installed') + states.add('all') + + if kind in states: + if simple: + print(f"{k:50}") + else: + print(f"{prefix} {k:50}(author: {v['author']})") + + +def show_snapshot(simple_mode=False): + json_obj = core.get_current_snapshot() + + if simple_mode: + print(f"[{json_obj['comfyui']}] comfyui") + for k, v in json_obj['git_custom_nodes'].items(): + print(f"[{v['hash']}] {k}") + for v in json_obj['file_custom_nodes']: + print(f"[ N/A ] {v['filename']}") + + else: + formatted_json = json.dumps(json_obj, ensure_ascii=False, indent=4) + print(formatted_json) + + +def show_snapshot_list(simple_mode=False): + path = os.path.join(comfyui_manager_path, 'snapshots') + + files = os.listdir(path) + json_files = [x for x in files if x.endswith('.json')] + for x in sorted(json_files): + print(x) + + +def cancel(): + if os.path.exists(script_path): + os.remove(script_path) + + if os.path.exists(restore_snapshot_path): + os.remove(restore_snapshot_path) + + +def for_each_nodes(act, allow_all=True): + global nodes + + is_all = False + if allow_all and 'all' in nodes: + is_all = True + nodes = [x for x in custom_node_map.keys() if os.path.exists(os.path.join(custom_nodes_path, x)) or os.path.exists(os.path.join(custom_nodes_path, x)+'.disabled')] + + for x in nodes: + try: + act(x, is_all=is_all) + except Exception as e: + print(f"ERROR: {e}") + traceback.print_exc() + + +op = sys.argv[1] + + +if op == 'install': + for_each_nodes(install_node) + +elif op == 'uninstall': + for_each_nodes(uninstall_node) + +elif op == 'update': + for_each_nodes(update_node, allow_all=True) + +elif op == 'disable': + for_each_nodes(disable_node, allow_all=True) + +elif op == 'enable': + for_each_nodes(enable_node, allow_all=True) + +elif op == 'fix': + for_each_nodes(fix_node, allow_all=True) + +elif op == 'show': + if sys.argv[2] == 'snapshot': + show_snapshot() + elif sys.argv[2] == 'snapshot-list': + show_snapshot_list() + else: + show_list(sys.argv[2]) + +elif op == 'simple-show': + if sys.argv[2] == 'snapshot': + show_snapshot(True) + elif sys.argv[2] == 'snapshot-list': + show_snapshot_list(True) + else: + show_list(sys.argv[2], True) + +elif op == 'cli-only-mode': + cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode') + if sys.argv[2] == 'enable': + with open(cli_mode_flag, 'w') as file: + pass + print(f"\ncli-only-mode: enabled\n") + elif sys.argv[2] == 'disable': + os.remove(cli_mode_flag) + print(f"\ncli-only-mode: disabled\n") + else: + print(f"\ninvalid value for cli-only-mode: {sys.argv[2]}\n") + +elif op == 'save-snapshot': + path = core.save_snapshot_with_postfix('snapshot') + print(f"Current snapshot is saved as `{path}`") + +elif op == 'restore-snapshot': + restore_snapshot(sys.argv[2]) + +elif op == 'clear': + cancel() + +else: + print(f"\nInvalid command `{op}`") + +print(f"") diff --git a/git_helper.py b/git_helper.py index dbe3271..37d6dcc 100644 --- a/git_helper.py +++ b/git_helper.py @@ -176,24 +176,32 @@ def checkout_custom_node_hash(git_custom_node_infos): if repo_name.endswith('.disabled'): repo_name = repo_name[:-9] - item = git_custom_node_infos[repo_name_to_url[repo_name]] - if item['disabled'] and is_disabled: - pass - elif item['disabled'] and not is_disabled: - # disable - print(f"DISABLE: {repo_name}") - new_path = fullpath + ".disabled" - os.rename(fullpath, new_path) - pass - elif not item['disabled'] and is_disabled: - # enable - print(f"ENABLE: {repo_name}") - new_path = fullpath[:-9] - os.rename(fullpath, new_path) - fullpath = new_path - need_checkout = True + if repo_name not in repo_name_to_url: + if not is_disabled: + # should be disabled + print(f"DISABLE: {repo_name}") + new_path = fullpath + ".disabled" + os.rename(fullpath, new_path) + need_checkout = False else: - need_checkout = True + item = git_custom_node_infos[repo_name_to_url[repo_name]] + if item['disabled'] and is_disabled: + pass + elif item['disabled'] and not is_disabled: + # disable + print(f"DISABLE: {repo_name}") + new_path = fullpath + ".disabled" + os.rename(fullpath, new_path) + + elif not item['disabled'] and is_disabled: + # enable + print(f"ENABLE: {repo_name}") + new_path = fullpath[:-9] + os.rename(fullpath, new_path) + fullpath = new_path + need_checkout = True + else: + need_checkout = True if need_checkout: repo = git.Repo(fullpath) @@ -202,6 +210,7 @@ def checkout_custom_node_hash(git_custom_node_infos): if commit_hash != item['hash']: print(f"CHECKOUT: {repo_name} [{item['hash']}]") repo.git.checkout(item['hash']) + except Exception: print(f"Failed to restore snapshots for the custom node '{path}'") diff --git a/glob/manager_core.py b/glob/manager_core.py index eb82769..1a9d356 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -21,8 +21,12 @@ sys.path.append(glob_path) import cm_global from manager_util import * +version = [2, 20] +version_str = f"V{version[0]}.{version[1]}" + (f'.{version[2]}' if len(version) > 2 else '') + comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, '..')) +comfy_path = os.path.abspath(os.path.join(custom_nodes_path, '..')) channel_list_path = os.path.join(comfyui_manager_path, 'channels.list') config_path = os.path.join(comfyui_manager_path, "config.ini") startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts") @@ -106,6 +110,33 @@ def is_blacklisted(name): return False +def is_installed(name): + name = name.strip() + + if name.startswith('#'): + return True + + pattern = r'([^<>!=]+)([<>!=]=?)(.*)' + match = re.search(pattern, name) + + if match: + name = match.group(1) + + if name in cm_global.pip_downgrade_blacklist: + pips = get_installed_packages() + + if match is None: + if name in pips: + return True + elif match.group(2) in ['<=', '==', '<']: + if name in pips: + if StrictVersion(pips[name]) >= StrictVersion(match.group(3)): + print(f"[ComfyUI-Manager] skip black listed pip installation: '{name}'") + return True + + return name.lower() in get_installed_packages() + + def get_channel_dict(): global channel_dict @@ -337,7 +368,7 @@ def __win_check_git_pull(path): process.wait() -def execute_install_script(url, repo_path, lazy_mode=False): +def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False): install_script_path = os.path.join(repo_path, "install.py") requirements_path = os.path.join(repo_path, "requirements.txt") @@ -353,12 +384,12 @@ def execute_install_script(url, repo_path, lazy_mode=False): if package_name: install_cmd = [sys.executable, "-m", "pip", "install", package_name] if package_name.strip() != "": - try_install_script(url, repo_path, install_cmd) + try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) 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) + try_install_script(url, repo_path, install_cmd, instant_execution=instant_execution) return True @@ -466,8 +497,8 @@ def is_valid_url(url): return False -def gitclone_install(files): - print(f"install: {files}") +def gitclone_install(files, instant_execution=False): + print(f"Install: {files}") for url in files: if not is_valid_url(url): print(f"Invalid git url: '{url}'") @@ -481,7 +512,7 @@ def gitclone_install(files): repo_path = os.path.join(custom_nodes_path, repo_name) # Clone the repository from the remote URL - if platform.system() == 'Windows': + if not instant_execution and platform.system() == 'Windows': res = manager_funcs.run_script([sys.executable, git_script_path, "--clone", custom_nodes_path, url]) if res != 0: return False @@ -490,7 +521,7 @@ def gitclone_install(files): repo.git.clear_cache() repo.close() - if not execute_install_script(url, repo_path): + if not execute_install_script(url, repo_path, instant_execution=instant_execution): return False except Exception as e: @@ -566,13 +597,17 @@ def is_file_created_within_one_day(file_path): return time_difference <= 86400 -async def get_data_by_mode(mode, filename): +async def get_data_by_mode(mode, filename, channel_url=None): try: if mode == "local": uri = os.path.join(comfyui_manager_path, filename) json_obj = await get_data(uri) else: - uri = get_config()['channel_url'] + '/' + filename + if channel_url is None: + uri = get_config()['channel_url'] + '/' + filename + else: + uri = channel_url + '/' + filename + cache_uri = str(simple_hash(uri))+'_'+filename cache_uri = os.path.join(cache_dir, cache_uri) @@ -585,7 +620,6 @@ async def get_data_by_mode(mode, filename): with open(cache_uri, "w", encoding='utf-8') as file: json.dump(json_obj, file, indent=4, sort_keys=True) else: - uri = get_config()['channel_url'] + '/' + filename json_obj = await get_data(uri) with cache_lock: with open(cache_uri, "w", encoding='utf-8') as file: @@ -611,6 +645,9 @@ def gitclone_fix(files): repo_name = os.path.splitext(os.path.basename(url))[0] repo_path = os.path.join(custom_nodes_path, repo_name) + if os.path.exists(repo_path+'.disabled'): + repo_path = repo_path+'.disabled' + if not execute_install_script(url, repo_path): return False @@ -653,7 +690,7 @@ def rmtree(path): def gitclone_uninstall(files): import os - print(f"uninstall: {files}") + print(f"Uninstall: {files}") for url in files: if url.endswith("/"): url = url[:-1] @@ -708,7 +745,7 @@ def gitclone_set_active(files, is_disable): dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") dir_path = os.path.join(custom_nodes_path, dir_name) - # safey check + # safety 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 @@ -739,7 +776,7 @@ def gitclone_set_active(files, is_disable): return True -def gitclone_update(files): +def gitclone_update(files, instant_execution=False, skip_script=False): import os print(f"Update: {files}") @@ -749,16 +786,26 @@ def gitclone_update(files): try: repo_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") repo_path = os.path.join(custom_nodes_path, repo_name) + + if os.path.exists(repo_path+'.disabled'): + repo_path = repo_path+'.disabled' + git_pull(repo_path) - if not execute_install_script(url, repo_path, lazy_mode=True): - return False + if not skip_script: + if instant_execution: + if not execute_install_script(url, repo_path, lazy_mode=False, instant_execution=True): + return False + else: + if not execute_install_script(url, repo_path, lazy_mode=True): + return False except Exception as e: print(f"Update(git-clone) error: {url} / {e}", file=sys.stderr) return False - print("Update was successful.") + if not skip_script: + print("Update was successful.") return True @@ -832,3 +879,73 @@ def check_a_custom_node_installed(item, do_fetch=False, do_update_check=True, do item['installed'] = 'Disabled' else: item['installed'] = 'False' + + +def get_current_snapshot(): + # Get ComfyUI hash + repo_path = comfy_path + + print(f"comfy_path: {comfy_path}") + 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 {} + + repo = git.Repo(repo_path) + comfyui_commit_hash = repo.head.commit.hexsha + + git_custom_nodes = {} + file_custom_nodes = [] + + # Get custom nodes hash + for path in os.listdir(custom_nodes_path): + fullpath = os.path.join(custom_nodes_path, path) + + if os.path.isdir(fullpath): + is_disabled = path.endswith(".disabled") + + try: + git_dir = os.path.join(fullpath, '.git') + + if not os.path.exists(git_dir): + continue + + repo = git.Repo(fullpath) + commit_hash = repo.head.commit.hexsha + url = repo.remotes.origin.url + git_custom_nodes[url] = { + 'hash': commit_hash, + 'disabled': is_disabled + } + + except: + print(f"Failed to extract snapshots for the custom node '{path}'.") + + elif path.endswith('.py'): + is_disabled = path.endswith(".py.disabled") + filename = os.path.basename(path) + item = { + 'filename': filename, + 'disabled': is_disabled + } + + file_custom_nodes.append(item) + + return { + 'comfyui': comfyui_commit_hash, + 'git_custom_nodes': git_custom_nodes, + 'file_custom_nodes': file_custom_nodes, + } + + +def save_snapshot_with_postfix(postfix): + now = datetime.now() + + date_time_format = now.strftime("%Y-%m-%d_%H-%M-%S") + file_name = f"{date_time_format}_{postfix}" + + path = os.path.join(comfyui_manager_path, 'snapshots', f"{file_name}.json") + with open(path, "w") as json_file: + json.dump(get_current_snapshot(), json_file, indent=4) + + return file_name+'.json' + diff --git a/glob/manager_server.py b/glob/manager_server.py new file mode 100644 index 0000000..4ee7584 --- /dev/null +++ b/glob/manager_server.py @@ -0,0 +1,1623 @@ +import mimetypes +import traceback + +import folder_paths +import locale +import subprocess # don't remove this +import concurrent +import nodes +import hashlib +import os +import sys +import threading +import re +import shutil +import git + +from server import PromptServer +import manager_core as core +import cm_global + +print(f"### Loading: ComfyUI-Manager ({core.version_str})") + +comfy_ui_hash = "-" + + +def handle_stream(stream, prefix): + stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace') + for msg in stream: + if prefix == '[!]' and ('it/s]' in msg or 's/it]' in msg) and ('%|' in msg or 'it [' in msg): + if msg.startswith('100%'): + print('\r' + msg, end="", file=sys.stderr), + else: + print('\r' + msg[:-1], end="", file=sys.stderr), + else: + if prefix == '[!]': + print(prefix, msg, end="", file=sys.stderr) + else: + print(prefix, msg, end="") + + +from comfy.cli_args import args +import latent_preview + + +class ManagerFuncsInComfyUI(core.ManagerFuncs): + def get_current_preview_method(self): + 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 run_script(self, cmd, cwd='.'): + if len(cmd) > 0 and cmd[0].startswith("#"): + print(f"[ComfyUI-Manager] Unexpected behavior: `{cmd}`") + return 0 + + 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() + + +core.manager_funcs = ManagerFuncsInComfyUI() + +sys.path.append('../..') + +from torchvision.datasets.utils import download_url + +core.comfy_path = os.path.dirname(folder_paths.__file__) +core.js_path = os.path.join(core.comfy_path, "web", "extensions") + +local_db_model = os.path.join(core.comfyui_manager_path, "model-list.json") +local_db_alter = os.path.join(core.comfyui_manager_path, "alter-list.json") +local_db_custom_node_list = os.path.join(core.comfyui_manager_path, "custom-node-list.json") +local_db_extension_node_mappings = os.path.join(core.comfyui_manager_path, "extension-node-map.json") +components_path = os.path.join(core.comfyui_manager_path, 'components') + + +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 + + core.get_config()['preview_method'] = args.preview_method + + +set_preview_method(core.get_config()['preview_method']) + + +def set_badge_mode(mode): + core.get_config()['badge_mode'] = mode + + +def set_default_ui_mode(mode): + core.get_config()['default_ui'] = mode + + +def set_component_policy(mode): + core.get_config()['component_policy'] = mode + + +def set_double_click_policy(mode): + core.get_config()['double_click_policy'] = mode + + +def print_comfyui_version(): + global comfy_ui_hash + + is_detached = False + try: + repo = git.Repo(os.path.dirname(folder_paths.__file__)) + core.comfy_ui_revision = len(list(repo.iter_commits('HEAD'))) + + comfy_ui_hash = repo.head.commit.hexsha + cm_global.variables['comfyui.revision'] = core.comfy_ui_revision + + core.comfy_ui_commit_datetime = repo.head.commit.committed_datetime + cm_global.variables['comfyui.commit_datetime'] = core.comfy_ui_commit_datetime + + is_detached = repo.head.is_detached + current_branch = repo.active_branch.name + + try: + if core.comfy_ui_commit_datetime.date() < core.comfy_ui_required_commit_datetime.date(): + print(f"\n\n## [WARN] ComfyUI-Manager: Your ComfyUI version ({core.comfy_ui_revision})[{core.comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version. ##\n\n") + except: + pass + + # process on_revision_detected --> + if 'cm.on_revision_detected_handler' in cm_global.variables: + for k, f in cm_global.variables['cm.on_revision_detected_handler']: + try: + f(core.comfy_ui_revision) + except Exception: + print(f"[ERROR] '{k}' on_revision_detected_handler") + traceback.print_exc() + + del cm_global.variables['cm.on_revision_detected_handler'] + else: + print(f"[ComfyUI-Manager] Some features are restricted due to your ComfyUI being outdated.") + # <-- + + if current_branch == "master": + print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'") + else: + print(f"### ComfyUI Revision: {core.comfy_ui_revision} on '{current_branch}' [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'") + except: + if is_detached: + print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] *DETACHED | Released on '{core.comfy_ui_commit_datetime.date()}'") + else: + print("### ComfyUI Revision: UNKNOWN (The currently installed ComfyUI is not a Git repository)") + + +print_comfyui_version() + + +async def populate_github_stats(json_obj, filename, silent=False): + uri = os.path.join(core.comfyui_manager_path, filename) + with open(uri, "r", encoding='utf-8') as f: + github_stats = json.load(f) + if 'custom_nodes' in json_obj: + for i, node in enumerate(json_obj['custom_nodes']): + url = node['reference'] + if url in github_stats: + json_obj['custom_nodes'][i]['stars'] = github_stats[url]['stars'] + json_obj['custom_nodes'][i]['last_update'] = github_stats[url]['last_update'] + else: + json_obj['custom_nodes'][i]['stars'] = -1 + json_obj['custom_nodes'][i]['last_update'] = -1 + return json_obj + + +def setup_environment(): + git_exe = core.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(core.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_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="") + + def process_custom_node(item): + core.check_a_custom_node_installed(item, do_fetch, do_update_check, do_update) + + with concurrent.futures.ThreadPoolExecutor(4) as executor: + for item in json_obj['custom_nodes']: + executor.submit(process_custom_node, item) + + 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.") + + +def nickname_filter(json_obj): + preemptions_map = {} + + for k, x in json_obj.items(): + if 'preemptions' in x[1]: + for y in x[1]['preemptions']: + preemptions_map[y] = k + elif k.endswith("/ComfyUI"): + for y in x[0]: + preemptions_map[y] = k + + updates = {} + for k, x in json_obj.items(): + removes = set() + for y in x[0]: + k2 = preemptions_map.get(y) + if k2 is not None and k != k2: + removes.add(y) + + if len(removes) > 0: + updates[k] = [y for y in x[0] if y not in removes] + + for k, v in updates.items(): + json_obj[k][0] = v + + return json_obj + + +@PromptServer.instance.routes.get("/customnode/getmappings") +async def fetch_customnode_mappings(request): + mode = request.rel_url.query["mode"] + + nickname_mode = False + if mode == "nickname": + mode = "local" + nickname_mode = True + + json_obj = await core.get_data_by_mode(mode, 'extension-node-map.json') + + if nickname_mode: + json_obj = nickname_filter(json_obj) + + all_nodes = set() + patterns = [] + for k, x in json_obj.items(): + all_nodes.update(set(x[0])) + + if 'nodename_pattern' in x[1]: + patterns.append((x[1]['nodename_pattern'], x[0])) + + missing_nodes = set(nodes.NODE_CLASS_MAPPINGS.keys()) - all_nodes + + for x in missing_nodes: + for pat, item in patterns: + if re.match(pat, x): + item.append(x) + + return web.json_response(json_obj, content_type='application/json') + + +@PromptServer.instance.routes.get("/customnode/fetch_updates") +async def fetch_updates(request): + try: + json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') + + 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) + + +@PromptServer.instance.routes.get("/customnode/update_all") +async def update_all(request): + try: + core.save_snapshot_with_postfix('autosave') + + json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') + + check_custom_nodes_installed(json_obj, do_update=True) + + updated = [item['title'] for item in json_obj['custom_nodes'] if item['installed'] == 'Update'] + failed = [item['title'] for item in json_obj['custom_nodes'] if item['installed'] == 'Fail'] + + res = {'updated': updated, 'failed': failed} + + if len(updated) == 0 and len(failed) == 0: + status = 200 + else: + status = 201 + + return web.json_response(res, status=status, content_type='application/json') + except: + return web.Response(status=400) + finally: + core.clear_pip_cache() + + +def convert_markdown_to_html(input_text): + pattern_a = re.compile(r'\[a/([^]]+)\]\(([^)]+)\)') + pattern_w = re.compile(r'\[w/([^]]+)\]') + pattern_i = re.compile(r'\[i/([^]]+)\]') + pattern_bold = re.compile(r'\*\*([^*]+)\*\*') + pattern_white = re.compile(r'%%([^*]+)%%') + + def replace_a(match): + return f"{match.group(1)}" + + def replace_w(match): + return f"

{match.group(1)}

" + + def replace_i(match): + return f"

{match.group(1)}

" + + def replace_bold(match): + return f"{match.group(1)}" + + def replace_white(match): + return f"{match.group(1)}" + + input_text = input_text.replace('\\[', '[').replace('\\]', ']').replace('<', '<').replace('>', '>') + + result_text = re.sub(pattern_a, replace_a, input_text) + result_text = re.sub(pattern_w, replace_w, result_text) + result_text = re.sub(pattern_i, replace_i, result_text) + result_text = re.sub(pattern_bold, replace_bold, result_text) + result_text = re.sub(pattern_white, replace_white, result_text) + + return result_text.replace("\n", "
") + + +def populate_markdown(x): + if 'description' in x: + x['description'] = convert_markdown_to_html(x['description']) + + if 'name' in x: + x['name'] = x['name'].replace('<', '<').replace('>', '>') + + if 'title' in x: + x['title'] = x['title'].replace('<', '<').replace('>', '>') + + +@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": + channel = 'local' + else: + channel = core.get_config()['channel_url'] + + json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') + json_obj = await populate_github_stats(json_obj, "github-stats.json") + + def is_ignored_notice(code): + global version + + if code is not None and code.startswith('#NOTICE_'): + try: + notice_version = [int(x) for x in code[8:].split('.')] + return notice_version[0] < version[0] or (notice_version[0] == version[0] and notice_version[1] <= version[1]) + except Exception: + return False + else: + return False + + json_obj['custom_nodes'] = [record for record in json_obj['custom_nodes'] if not is_ignored_notice(record.get('author'))] + + check_custom_nodes_installed(json_obj, False, not skip_update) + + for x in json_obj['custom_nodes']: + populate_markdown(x) + + if channel != 'local': + found = 'custom' + + for name, url in core.get_channel_dict().items(): + if url == channel: + found = name + break + + channel = found + + json_obj['channel'] = channel + + return web.json_response(json_obj, content_type='application/json') + + +@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 + + alter_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'alter-list.json') + custom_node_json = await core.get_data_by_mode(request.rel_url.query["mode"], 'custom-node-list.json') + + 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] + core.check_a_custom_node_installed(custom_node, not skip_update) + + populate_markdown(item) + populate_markdown(custom_node) + item['custom_node'] = custom_node + + return web.json_response(alter_json, content_type='application/json') + + +def check_model_installed(json_obj): + def process_model(item): + model_path = get_model_path(item) + item['installed'] = 'None' + + if model_path is not None: + if os.path.exists(model_path): + item['installed'] = 'True' + else: + item['installed'] = 'False' + + with concurrent.futures.ThreadPoolExecutor(8) as executor: + for item in json_obj['models']: + executor.submit(process_model, item) + + +@PromptServer.instance.routes.get("/externalmodel/getlist") +async def fetch_externalmodel_list(request): + json_obj = await core.get_data_by_mode(request.rel_url.query["mode"], 'model-list.json') + + check_model_installed(json_obj) + + for x in json_obj['models']: + populate_markdown(x) + + return web.json_response(json_obj, content_type='application/json') + + +@PromptServer.instance.routes.get("/snapshot/get_current") +async def get_snapshot_list(request): + json_obj = core.get_current_snapshot() + return web.json_response(json_obj, content_type='application/json') + + +@PromptServer.instance.routes.get("/snapshot/getlist") +async def get_snapshot_list(request): + snapshots_directory = os.path.join(core.comfyui_manager_path, 'snapshots') + items = [f[:-5] for f in os.listdir(snapshots_directory) if f.endswith('.json')] + items.sort(reverse=True) + return web.json_response({'items': items}, content_type='application/json') + + +@PromptServer.instance.routes.get("/snapshot/remove") +async def remove_snapshot(request): + try: + target = request.rel_url.query["target"] + + path = os.path.join(core.comfyui_manager_path, 'snapshots', f"{target}.json") + if os.path.exists(path): + os.remove(path) + + return web.Response(status=200) + except: + return web.Response(status=400) + + +@PromptServer.instance.routes.get("/snapshot/restore") +async def remove_snapshot(request): + try: + target = request.rel_url.query["target"] + + path = os.path.join(core.comfyui_manager_path, 'snapshots', f"{target}.json") + if os.path.exists(path): + if not os.path.exists(core.startup_script_path): + os.makedirs(core.startup_script_path) + + target_path = os.path.join(core.startup_script_path, "restore-snapshot.json") + shutil.copy(path, target_path) + + print(f"Snapshot restore scheduled: `{target}`") + return web.Response(status=200) + + print(f"Snapshot file not found: `{path}`") + return web.Response(status=400) + except: + return web.Response(status=400) + + +@PromptServer.instance.routes.get("/snapshot/get_current") +async def get_current_snapshot_api(request): + try: + return web.json_response(core.get_current_snapshot(), content_type='application/json') + except: + return web.Response(status=400) + + +@PromptServer.instance.routes.get("/snapshot/save") +async def save_snapshot(request): + try: + core.save_snapshot_with_postfix('snapshot') + return web.Response(status=200) + except: + return web.Response(status=400) + + +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(core.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, core.custom_nodes_path) + else: + path = os.path.join(core.js_path, js_path_name) if js_path_name is not None else core.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 = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.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 = core.custom_nodes_path if url.endswith('.py') else os.path.join(core.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 + + +@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 = core.gitclone_install(json_data['files']) + + if 'pip' in json_data: + for pname in json_data['pip']: + pkg = core.remap_pip_package(pname) + install_cmd = [sys.executable, "-m", "pip", "install", pkg] + core.try_install_script(json_data['files'][0], ".", install_cmd) + + core.clear_pip_cache() + + if res: + print(f"After restarting ComfyUI, please refresh the browser.") + return web.json_response({}, content_type='application/json') + + return web.Response(status=400) + + +@PromptServer.instance.routes.post("/customnode/fix") +async def fix_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 == "git-clone": + res = core.gitclone_fix(json_data['files']) + else: + return web.Response(status=400) + + if 'pip' in json_data: + for pname in json_data['pip']: + install_cmd = [sys.executable, "-m", "pip", "install", '-U', pname] + core.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) + + +@PromptServer.instance.routes.get("/customnode/install/git_url") +async def install_custom_node_git_url(request): + res = False + if "url" in request.rel_url.query: + url = request.rel_url.query['url'] + res = core.gitclone_install([url]) + + if res: + print(f"After restarting ComfyUI, please refresh the browser.") + return web.Response(status=200) + + return web.Response(status=400) + + +@PromptServer.instance.routes.get("/customnode/install/pip") +async def install_custom_node_git_url(request): + res = False + if "packages" in request.rel_url.query: + packages = request.rel_url.query['packages'] + core.pip_install(packages.split(' ')) + + return web.Response(status=200) + + +@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 = core.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) + + +@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 = core.gitclone_update(json_data['files']) + + core.clear_pip_cache() + + if res: + print(f"After restarting ComfyUI, please refresh the browser.") + return web.json_response({}, content_type='application/json') + + return web.Response(status=400) + + +@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) + + if repo.head.is_detached: + core.switch_to_default_branch(repo) + + current_branch = repo.active_branch + branch_name = current_branch.name + + if current_branch.tracking_branch() is None: + print(f"[ComfyUI-Manager] There is no tracking branch ({current_branch})") + remote_name = 'origin' + else: + remote_name = current_branch.tracking_branch().remote_name + remote = repo.remote(name=remote_name) + + try: + remote.fetch() + except Exception as e: + if 'detected dubious' in str(e): + print(f"[ComfyUI-Manager] Try fixing 'dubious repository' error on 'ComfyUI' repository") + safedir_path = core.comfy_path.replace('\\', '/') + subprocess.run(['git', 'config', '--global', '--add', 'safe.directory', safedir_path]) + try: + remote.fetch() + except Exception: + print(f"\n[ComfyUI-Manager] Failed to fixing repository setup. Please execute this command on cmd: \n" + f"-----------------------------------------------------------------------------------------\n" + f'git config --global --add safe.directory "{safedir_path}"\n' + f"-----------------------------------------------------------------------------------------\n") + + commit_hash = repo.head.commit.hexsha + remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha + + if commit_hash != remote_commit_hash: + core.git_pull(repo_path) + core.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) + + +@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 = core.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) + + +@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}'") + + model_url = json_data['url'] + if not core.get_config()['model_download_by_agent'] and ( + model_url.startswith('https://github.com') or model_url.startswith('https://huggingface.co') or model_url.startswith('https://heibox.uni-heidelberg.de')): + model_dir = get_model_dir(json_data) + download_url(model_url, model_dir, filename=json_data['filename']) + + return web.json_response({}, content_type='application/json') + else: + res = download_url_with_agent(model_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) + + +class ManagerTerminalHook: + def write_stderr(self, msg): + PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg}) + + def write_stdout(self, msg): + PromptServer.instance.send_sync("manager-terminal-feedback", {"data": msg}) + + +manager_terminal_hook = ManagerTerminalHook() + + +@PromptServer.instance.routes.get("/manager/terminal") +async def terminal_mode(request): + if "mode" in request.rel_url.query: + if request.rel_url.query['mode'] == 'true': + sys.__comfyui_manager_terminal_hook.add_hook('cm', manager_terminal_hook) + else: + sys.__comfyui_manager_terminal_hook.remove_hook('cm') + + return web.Response(status=200) + + +@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']) + core.write_config() + else: + return web.Response(text=core.manager_funcs.get_current_preview_method(), status=200) + + return web.Response(status=200) + + +@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']) + core.write_config() + else: + return web.Response(text=core.get_config()['badge_mode'], status=200) + + return web.Response(status=200) + + +@PromptServer.instance.routes.get("/manager/default_ui") +async def default_ui_mode(request): + if "value" in request.rel_url.query: + set_default_ui_mode(request.rel_url.query['value']) + core.write_config() + else: + return web.Response(text=core.get_config()['default_ui'], status=200) + + return web.Response(status=200) + + +@PromptServer.instance.routes.get("/manager/component/policy") +async def component_policy(request): + if "value" in request.rel_url.query: + set_component_policy(request.rel_url.query['value']) + core.write_config() + else: + return web.Response(text=core.get_config()['component_policy'], status=200) + + return web.Response(status=200) + + +@PromptServer.instance.routes.get("/manager/dbl_click/policy") +async def dbl_click_policy(request): + if "value" in request.rel_url.query: + set_double_click_policy(request.rel_url.query['value']) + core.write_config() + else: + return web.Response(text=core.get_config()['double_click_policy'], status=200) + + return web.Response(status=200) + + +@PromptServer.instance.routes.get("/manager/channel_url_list") +async def channel_url_list(request): + channels = core.get_channel_dict() + if "value" in request.rel_url.query: + channel_url = channels.get(request.rel_url.query['value']) + if channel_url is not None: + core.get_config()['channel_url'] = channel_url + core.write_config() + else: + selected = 'custom' + selected_url = core.get_config()['channel_url'] + + for name, url in channels.items(): + if url == selected_url: + selected = name + break + + res = {'selected': selected, + 'list': core.get_channel_list()} + return web.json_response(res, status=200) + + return web.Response(status=200) + + +@PromptServer.instance.routes.get("/manager/notice") +async def get_notice(request): + url = "github.com" + path = "/ltdrdata/ltdrdata.github.io/wiki/News" + + async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: + async with session.get(f"https://{url}{path}") as response: + if response.status == 200: + # html_content = response.read().decode('utf-8') + html_content = await response.text() + + pattern = re.compile(r'
([\s\S]*?)
') + match = pattern.search(html_content) + + if match: + markdown_content = match.group(1) + markdown_content += f"
ComfyUI: {core.comfy_ui_revision}[{comfy_ui_hash[:6]}]({core.comfy_ui_commit_datetime.date()})" + # markdown_content += f"
         ()" + markdown_content += f"
Manager: {version_str}" + + try: + if core.comfy_ui_required_commit_datetime.date() > core.comfy_ui_commit_datetime.date(): + markdown_content = f'

Your ComfyUI is too OUTDATED!!!

' + markdown_content + except: + pass + + return web.Response(text=markdown_content, status=200) + else: + return web.Response(text="Unable to retrieve Notice", status=200) + else: + return web.Response(text="Unable to retrieve Notice", status=200) + + +@PromptServer.instance.routes.get("/manager/reboot") +def restart(self): + try: + sys.stdout.close_log() + except Exception as e: + pass + + return os.execv(sys.executable, [sys.executable] + sys.argv) + + +def sanitize_filename(input_string): + # 알파벳, 숫자, 및 밑줄 이외의 문자를 밑줄로 대체 + result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string) + return result_string + + +@PromptServer.instance.routes.post("/manager/component/save") +async def save_component(request): + try: + data = await request.json() + name = data['name'] + workflow = data['workflow'] + + if not os.path.exists(components_path): + os.mkdir(components_path) + + if 'packname' in workflow and workflow['packname'] != '': + sanitized_name = sanitize_filename(workflow['packname']) + '.pack' + else: + sanitized_name = sanitize_filename(name) + '.json' + + filepath = os.path.join(components_path, sanitized_name) + components = {} + if os.path.exists(filepath): + with open(filepath) as f: + components = json.load(f) + + components[name] = workflow + + with open(filepath, 'w') as f: + json.dump(components, f, indent=4, sort_keys=True) + return web.Response(text=filepath, status=200) + except: + return web.Response(status=400) + + +@PromptServer.instance.routes.post("/manager/component/loads") +async def load_components(request): + try: + json_files = [f for f in os.listdir(components_path) if f.endswith('.json')] + pack_files = [f for f in os.listdir(components_path) if f.endswith('.pack')] + + components = {} + for json_file in json_files + pack_files: + file_path = os.path.join(components_path, json_file) + with open(file_path, 'r') as file: + try: + # When there is a conflict between the .pack and the .json, the pack takes precedence and overrides. + components.update(json.load(file)) + except json.JSONDecodeError as e: + print(f"[ComfyUI-Manager] Error decoding component file in file {json_file}: {e}") + + return web.json_response(components) + except Exception as e: + print(f"[ComfyUI-Manager] failed to load components\n{e}") + return web.Response(status=400) + + +@PromptServer.instance.routes.get("/manager/share_option") +async def share_option(request): + if "value" in request.rel_url.query: + core.get_config()['share_option'] = request.rel_url.query['value'] + core.write_config() + else: + return web.Response(text=core.get_config()['share_option'], status=200) + + return web.Response(status=200) + + +def get_openart_auth(): + if not os.path.exists(os.path.join(core.comfyui_manager_path, ".openart_key")): + return None + try: + with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "r") as f: + openart_key = f.read().strip() + return openart_key if openart_key else None + except: + return None + + +def get_matrix_auth(): + if not os.path.exists(os.path.join(core.comfyui_manager_path, "matrix_auth")): + return None + try: + with open(os.path.join(core.comfyui_manager_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, + } + except: + return None + + +def get_comfyworkflows_auth(): + if not os.path.exists(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey")): + return None + try: + with open(os.path.join(core.comfyui_manager_path, "comfyworkflows_sharekey"), "r") as f: + share_key = f.read() + if not share_key.strip(): + return None + return share_key + except: + return None + + +def get_youml_settings(): + if not os.path.exists(os.path.join(core.comfyui_manager_path, ".youml")): + return None + try: + with open(os.path.join(core.comfyui_manager_path, ".youml"), "r") as f: + youml_settings = f.read().strip() + return youml_settings if youml_settings else None + except: + return None + + +def set_youml_settings(settings): + with open(os.path.join(core.comfyui_manager_path, ".youml"), "w") as f: + f.write(settings) + + +@PromptServer.instance.routes.get("/manager/get_openart_auth") +async def api_get_openart_auth(request): + # print("Getting stored Matrix credentials...") + openart_key = get_openart_auth() + if not openart_key: + return web.Response(status=404) + return web.json_response({"openart_key": openart_key}) + + +@PromptServer.instance.routes.post("/manager/set_openart_auth") +async def api_set_openart_auth(request): + json_data = await request.json() + openart_key = json_data['openart_key'] + with open(os.path.join(core.comfyui_manager_path, ".openart_key"), "w") as f: + f.write(openart_key) + return web.Response(status=200) + + +@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) + + +@PromptServer.instance.routes.get("/manager/youml/settings") +async def api_get_youml_settings(request): + youml_settings = get_youml_settings() + if not youml_settings: + return web.Response(status=404) + return web.json_response(json.loads(youml_settings)) + + +@PromptServer.instance.routes.post("/manager/youml/settings") +async def api_set_youml_settings(request): + json_data = await request.json() + set_youml_settings(json.dumps(json_data)) + return web.Response(status=200) + + +@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}) + + +args.enable_cors_header = "*" +if hasattr(PromptServer.instance, "app"): + app = PromptServer.instance.app + cors_middleware = server.create_cors_middleware(args.enable_cors_header) + app.middlewares.append(cors_middleware) + + +@PromptServer.instance.routes.post("/manager/set_esheep_workflow_and_images") +async def set_esheep_workflow_and_images(request): + json_data = await request.json() + current_workflow = json_data['workflow'] + images = json_data['images'] + with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), "w", encoding='utf-8') as file: + json.dump(json_data, file, indent=4) + return web.Response(status=200) + + +@PromptServer.instance.routes.get("/manager/get_esheep_workflow_and_images") +async def get_esheep_workflow_and_images(request): + with open(os.path.join(core.comfyui_manager_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file: + data = json.load(file) + return web.Response(status=200, text=json.dumps(data)) + + +def set_matrix_auth(json_data): + homeserver = json_data['homeserver'] + username = json_data['username'] + password = json_data['password'] + with open(os.path.join(core.comfyui_manager_path, "matrix_auth"), "w") as f: + f.write("\n".join([homeserver, username, password])) + + +def set_comfyworkflows_auth(comfyworkflows_sharekey): + with open(os.path.join(core.comfyui_manager_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() + + +def extract_model_file_names(json_data): + """Extract unique file names from the input JSON data.""" + file_names = set() + model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'} + + # Recursively search for file names in the JSON data + def recursive_search(data): + if isinstance(data, dict): + for value in data.values(): + recursive_search(value) + elif isinstance(data, list): + for item in data: + recursive_search(item) + elif isinstance(data, str) and '.' in data: + file_names.add(os.path.basename(data)) # file_names.add(data) + + recursive_search(json_data) + return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions] + + +def find_file_paths(base_dir, file_names): + """Find the paths of the files in the base directory.""" + file_paths = {} + + for root, dirs, files in os.walk(base_dir): + # Exclude certain directories + dirs[:] = [d for d in dirs if d not in ['.git']] + + for file in files: + if file in file_names: + file_paths[file] = os.path.join(root, file) + return file_paths + + +def compute_sha256_checksum(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() + + +@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'] + selected_output_index = json_data['selected_output_index'] + + try: + output_to_share = potential_outputs[int(selected_output_index)] + except: + # for now, pick the first output + output_to_share = potential_outputs[0] + + assert output_to_share['type'] in ('image', 'output') + output_dir = folder_paths.get_output_directory() + + if output_to_share['type'] == 'image': + asset_filename = output_to_share['image']['filename'] + asset_subfolder = output_to_share['image']['subfolder'] + + if output_to_share['image']['type'] == 'temp': + output_dir = folder_paths.get_temp_directory() + else: + asset_filename = output_to_share['output']['filename'] + asset_subfolder = output_to_share['output']['subfolder'] + + 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] + + share_website_host = "UNKNOWN" + 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, + "workflowJsonFileName": 'workflow.json', + "workflowJsonFileType": 'application/json', + }, + ) as resp: + assert resp.status == 200 + presigned_urls_json = await resp.json() + assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"] + assetFileKey = presigned_urls_json["assetFileKey"] + workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"] + workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"] + + # 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 + + # upload workflow json + async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session: + async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp: + assert resp.status == 200 + + model_filenames = extract_model_file_names(prompt['workflow']) + model_file_paths = find_file_paths(folder_paths.base_path, model_filenames) + + models_info = {} + for filename, filepath in model_file_paths.items(): + models_info[filename] = { + "filename": filename, + "sha256_checksum": compute_sha256_checksum(filepath), + "relative_path": os.path.relpath(filepath, folder_paths.base_path), + } + + # 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("workflowJsonFileKey", workflowJsonFileKey) + 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()) + form.add_field("currentSnapshot", json.dumps(core.get_current_snapshot())) + form.add_field("modelsInfo", json.dumps(models_info)) + + 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'] + + workflow_json_mxc_url = matrix.media_upload(prompt['workflow'], 'application/json', filename='workflow.json')['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') + response = matrix.send_content(comfyui_share_room_id, workflow_json_mxc_url, 'workflow.json', 'm.file') + 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) + + +def sanitize(data): + return data.replace("<", "<").replace(">", ">") + + +async def _confirm_try_install(sender, custom_node_url, msg): + json_obj = await core.get_data_by_mode('default', 'custom-node-list.json') + + sender = sanitize(sender) + msg = sanitize(msg) + target = core.lookup_customnode_by_url(json_obj, custom_node_url) + + if target is not None: + PromptServer.instance.send_sync("cm-api-try-install-customnode", + {"sender": sender, "target": target, "msg": msg}) + else: + print(f"[ComfyUI Manager API] Failed to try install - Unknown custom node url '{custom_node_url}'") + + +def confirm_try_install(sender, custom_node_url, msg): + asyncio.run(_confirm_try_install(sender, custom_node_url, msg)) + + +cm_global.register_api('cm.try-install-custom-node', confirm_try_install) + +import asyncio + + +async def default_cache_update(): + async def get_cache(filename): + uri = 'https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/' + filename + cache_uri = str(core.simple_hash(uri)) + '_' + filename + cache_uri = os.path.join(core.cache_dir, cache_uri) + + json_obj = await core.get_data(uri, True) + + with core.cache_lock: + with open(cache_uri, "w", encoding='utf-8') as file: + json.dump(json_obj, file, indent=4, sort_keys=True) + print(f"[ComfyUI-Manager] default cache updated: {uri}") + + a = get_cache("custom-node-list.json") + b = get_cache("extension-node-map.json") + c = get_cache("model-list.json") + d = get_cache("alter-list.json") + + await asyncio.gather(a, b, c, d) + + +threading.Thread(target=lambda: asyncio.run(default_cache_update())).start() + +if not os.path.exists(core.config_path): + core.get_config() + core.write_config() + + +cm_global.register_extension('ComfyUI-Manager', + {'version': core.version, + 'name': 'ComfyUI Manager', + 'nodes': {'Terminal Log //CM'}, + 'description': 'It provides the ability to manage custom nodes in ComfyUI.', }) + diff --git a/prestartup_script.py b/prestartup_script.py index e419f30..c3476c9 100644 --- a/prestartup_script.py +++ b/prestartup_script.py @@ -459,40 +459,39 @@ if os.path.exists(restore_snapshot_path): cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path] exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture) - with open(restore_snapshot_path, 'r', encoding="UTF-8", errors="ignore") as json_file: - info = json.load(json_file) - for url in cloned_repos: - try: - repository_name = url.split("/")[-1].strip() - repo_path = os.path.join(custom_nodes_path, repository_name) - repo_path = os.path.abspath(repo_path) - - requirements_path = os.path.join(repo_path, 'requirements.txt') - install_script_path = os.path.join(repo_path, 'install.py') - - this_exit_code = 0 - - if os.path.exists(requirements_path): - with open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file: - for line in file: - package_name = remap_pip_package(line.strip()) - if package_name and not is_installed(package_name): - install_cmd = [sys.executable, "-m", "pip", "install", package_name] - this_exit_code += process_wrap(install_cmd, repo_path) - - if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install: - processed_install.add(f'{repo_path}/install.py') - install_cmd = [sys.executable, install_script_path] - print(f">>> {install_cmd} / {repo_path}") - this_exit_code += process_wrap(install_cmd, repo_path) - - if this_exit_code != 0: - print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.") - - except Exception as e: - print(e) + repository_name = '' + for url in cloned_repos: + try: + repository_name = url.split("/")[-1].strip() + repo_path = os.path.join(custom_nodes_path, repository_name) + repo_path = os.path.abspath(repo_path) + + requirements_path = os.path.join(repo_path, 'requirements.txt') + install_script_path = os.path.join(repo_path, 'install.py') + + this_exit_code = 0 + + if os.path.exists(requirements_path): + with open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file: + for line in file: + package_name = remap_pip_package(line.strip()) + if package_name and not is_installed(package_name): + install_cmd = [sys.executable, "-m", "pip", "install", package_name] + this_exit_code += process_wrap(install_cmd, repo_path) + + if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install: + processed_install.add(f'{repo_path}/install.py') + install_cmd = [sys.executable, install_script_path] + print(f">>> {install_cmd} / {repo_path}") + this_exit_code += process_wrap(install_cmd, repo_path) + + if this_exit_code != 0: print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.") + except Exception as e: + print(e) + print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.") + if exit_code != 0: print(f"[ComfyUI-Manager] Restore snapshot failed.") else: