diff --git a/glob/manager_core.py b/glob/manager_core.py index b956f1f4..811589ed 100644 --- a/glob/manager_core.py +++ b/glob/manager_core.py @@ -31,6 +31,8 @@ sys.path.append(glob_path) import cm_global import cnr_utils import manager_util +import manager_downloader + version_code = [3, 0] version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '') @@ -872,7 +874,7 @@ class UnifiedManager: archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution download_path = os.path.join(get_default_custom_nodes_path(), archive_name) - manager_util.download_url(node_info.download_url, get_default_custom_nodes_path(), archive_name) + manager_downloader.download_url(node_info.download_url, get_default_custom_nodes_path(), archive_name) # 2. extract files into @ install_path = self.active_nodes[node_id][1] @@ -1142,7 +1144,7 @@ class UnifiedManager: if os.path.exists(install_path): return result.fail(f'Install path already exists: {install_path}') - manager_util.download_url(node_info.download_url, get_default_custom_nodes_path(), archive_name) + manager_downloader.download_url(node_info.download_url, get_default_custom_nodes_path(), archive_name) os.makedirs(install_path, exist_ok=True) extracted = manager_util.extract_package_as_zip(download_path, install_path) os.remove(download_path) @@ -2232,6 +2234,19 @@ def lookup_customnode_by_url(data, target): return None +def lookup_installed_custom_nodes_legacy(repo_name): + base_paths = get_custom_nodes_paths() + + for base_path in base_paths: + repo_path = os.path.join(base_path, repo_name) + if os.path.exists(repo_path): + return True, repo_path + elif os.path.exists(repo_path + '.disabled'): + return False, repo_path + + return None + + def simple_check_custom_node(url): dir_name = os.path.splitext(os.path.basename(url))[0].replace(".git", "") dir_path = os.path.join(get_default_custom_nodes_path(), dir_name) diff --git a/glob/manager_downloader.py b/glob/manager_downloader.py index 8a8c73c0..af7cc054 100644 --- a/glob/manager_downloader.py +++ b/glob/manager_downloader.py @@ -1,5 +1,7 @@ import os from urllib.parse import urlparse +import urllib +import sys aria2 = os.getenv('COMFYUI_MANAGER_ARIA2_SERVER') HF_ENDPOINT = os.getenv('HF_ENDPOINT') @@ -14,12 +16,32 @@ if aria2 is not None: aria2 = aria2p.API(aria2p.Client(host=host, port=port, secret=secret)) +def basic_download_url(url, dest_folder, filename): + import requests + + # Ensure the destination folder exists + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + # Full path to save the file + dest_path = os.path.join(dest_folder, filename) + + # Download the file + response = requests.get(url, stream=True) + if response.status_code == 200: + with open(dest_path, 'wb') as file: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + file.write(chunk) + else: + raise Exception(f"Failed to download file from {url}") + + def download_url(model_url: str, model_dir: str, filename: str): if aria2: return aria2_download_url(model_url, model_dir, filename) else: from torchvision.datasets.utils import download_url as torchvision_download_url - return torchvision_download_url(model_url, model_dir, filename) @@ -68,3 +90,26 @@ def aria2_download_url(model_url: str, model_dir: str, filename: str): progress_bar.update(download.completed_length - progress_bar.n) time.sleep(1) download.update() + + +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 \ No newline at end of file diff --git a/glob/manager_server.py b/glob/manager_server.py index 276defc5..90335440 100644 --- a/glob/manager_server.py +++ b/glob/manager_server.py @@ -17,6 +17,7 @@ from server import PromptServer import manager_core as core import manager_util import cm_global +import logging print(f"### Loading: ComfyUI-Manager ({core.version_str})") @@ -125,7 +126,7 @@ core.manager_funcs = ManagerFuncsInComfyUI() sys.path.append('../..') -from manager_downloader import download_url +from manager_downloader import download_url, download_url_with_agent core.comfy_path = os.path.dirname(folder_paths.__file__) core.js_path = os.path.join(core.comfy_path, "web", "extensions") @@ -187,14 +188,11 @@ def print_comfyui_version(): is_detached = repo.head.is_detached current_branch = repo.active_branch.name - if current_branch == "master": - comfyui_tag = repo.git.describe('--tags', repo.heads.main.commit.hexsha) - if not comfyui_tag.startswith("v"): - comfyui_tag = None + comfyui_tag = core.get_comfyui_tag() 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.get_comfyui_tag()})[{core.comfy_ui_commit_datetime.date()}] is too old. Please update to the latest version. ##\n\n") + 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 @@ -213,13 +211,15 @@ def print_comfyui_version(): # <-- if current_branch == "master": - version_tag = core.get_comfyui_tag() - if version_tag is None: - print(f"### ComfyUI Revision: {core.comfy_ui_revision} [{comfy_ui_hash[:8]}] | Released on '{core.comfy_ui_commit_datetime.date()}'") + if comfyui_tag: + print(f"### ComfyUI Version: {comfyui_tag} | Released on '{core.comfy_ui_commit_datetime.date()}'") else: - print(f"### ComfyUI Version: {core.get_comfyui_tag()} | Released on '{core.comfy_ui_commit_datetime.date()}'") + 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()}'") + if comfyui_tag: + print(f"### ComfyUI Version: {comfyui_tag} on '{current_branch}' | 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()}'") @@ -231,6 +231,7 @@ print_comfyui_version() core.check_invalid_nodes() + def setup_environment(): git_exe = core.get_config()['git_exe'] @@ -249,7 +250,7 @@ import zipfile import urllib.request -def get_model_dir(data): +def get_model_dir(data, show_log=False): if 'download_model_base' in folder_paths.folder_names_and_paths: models_base = folder_paths.folder_names_and_paths['download_model_base'][0][0] else: @@ -258,7 +259,9 @@ def get_model_dir(data): def resolve_custom_node(save_path): save_path = save_path[13:] # remove 'custom_nodes/' repo_name = save_path.replace('\\','/').split('/')[0] # get custom node repo name - repo_path = core.lookup_installed_custom_nodes(repo_name) + + # NOTE: The creation of files within the custom node path should be removed in the future. + repo_path = core.lookup_installed_custom_nodes_legacy(repo_name) if repo_path is not None and repo_path[0]: # Returns the retargeted path based on the actually installed repository return os.path.join(os.path.dirname(repo_path[1]), save_path) @@ -267,13 +270,15 @@ 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'.") + if show_log: + logging.info(f"[WARN] '{data['save_path']}' is not allowed path. So it will be saved into 'models/etc'.") base_model = os.path.join(models_base, "etc") else: if data['save_path'].startswith("custom_nodes"): base_model = resolve_custom_node(data['save_path']) if base_model is None: - print(f"[ComfyUI-Manager] The target custom node for model download is not installed: {data['save_path']}") + if show_log: + logging.info(f"[ComfyUI-Manager] The target custom node for model download is not installed: {data['save_path']}") return None else: base_model = os.path.join(models_base, data['save_path']) @@ -287,7 +292,8 @@ def get_model_dir(data): if folder_paths.folder_names_and_paths.get("text_encoders"): base_model = folder_paths.folder_names_and_paths["text_encoders"][0][0] else: - print("[ComfyUI-Manager] Your ComfyUI is outdated version.") + if show_log: + logging.info("[ComfyUI-Manager] Your ComfyUI is outdated version.") base_model = folder_paths.folder_names_and_paths["clip"][0][0] # outdated version elif model_type == "VAE": base_model = folder_paths.folder_names_and_paths["vae"][0][0] @@ -311,7 +317,8 @@ def get_model_dir(data): if folder_paths.folder_names_and_paths.get("diffusion_models"): base_model = folder_paths.folder_names_and_paths["diffusion_models"][0][1] else: - print("[ComfyUI-Manager] Your ComfyUI is outdated version.") + if show_log: + logging.info("[ComfyUI-Manager] Your ComfyUI is outdated version.") base_model = folder_paths.folder_names_and_paths["unet"][0][0] # outdated version else: base_model = os.path.join(models_base, "etc") @@ -319,8 +326,8 @@ def get_model_dir(data): return base_model -def get_model_path(data): - base_model = get_model_dir(data) +def get_model_path(data, show_log=False): + base_model = get_model_dir(data, show_log) if base_model is None: return None else: @@ -488,13 +495,13 @@ async def update_all(request): traceback.print_exc() return web.Response(status=400) finally: - core.clear_pip_cache() + manager_util.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_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'%%([^*]+)%%') @@ -594,7 +601,7 @@ async def fetch_customnode_alternatives(request): def check_model_installed(json_obj): def process_model(item): - model_path = get_model_path(item) + model_path = get_model_path(item, False) item['installed'] = 'None' if model_path is not None: @@ -627,7 +634,7 @@ async def fetch_externalmodel_list(request): @PromptServer.instance.routes.get("/snapshot/getlist") async def get_snapshot_list(request): - snapshots_directory = os.path.join(core.comfyui_manager_path, 'snapshots') + snapshots_directory = os.path.join(manager_util.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') @@ -638,11 +645,11 @@ async def remove_snapshot(request): if not is_allowed_security_level('middle'): print(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response(status=403) - + try: target = request.rel_url.query["target"] - path = os.path.join(core.comfyui_manager_path, 'snapshots', f"{target}.json") + path = os.path.join(manager_util.comfyui_manager_path, 'snapshots', f"{target}.json") if os.path.exists(path): os.remove(path) @@ -656,11 +663,11 @@ async def restore_snapshot(request): if not is_allowed_security_level('middle'): print(SECURITY_MESSAGE_MIDDLE_OR_BELOW) return web.Response(status=403) - + try: target = request.rel_url.query["target"] - path = os.path.join(core.comfyui_manager_path, 'snapshots', f"{target}.json") + path = os.path.join(manager_util.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) @@ -711,7 +718,7 @@ def unzip_install(files): f.write(data) with zipfile.ZipFile(temp_filename, 'r') as zip_ref: - zip_ref.extractall(core.custom_nodes_path) + zip_ref.extractall(core.get_default_custom_nodes_path()) os.remove(temp_filename) except Exception as e: @@ -722,29 +729,6 @@ def unzip_install(files): 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("/"): @@ -752,7 +736,7 @@ def copy_install(files, js_path_name=None): try: filename = os.path.basename(url) if url.endswith(".py"): - download_url(url, core.custom_nodes_path, filename) + download_url(url, core.get_default_custom_nodes_path(), filename) 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): @@ -772,7 +756,7 @@ def copy_uninstall(files, js_path_name='.'): 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) + base_path = core.get_default_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: @@ -798,7 +782,7 @@ def copy_set_active(files, is_disable, js_path_name='.'): 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) + base_path = core.get_default_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: @@ -857,7 +841,7 @@ async def reinstall_custom_node(request): async def install_custom_node(request): if not is_allowed_security_level('middle'): print(SECURITY_MESSAGE_MIDDLE_OR_BELOW) - return web.Response(status=403) + return web.Response(status=403, text="A security error has occurred. Please check the terminal logs") json_data = await request.json() @@ -884,28 +868,28 @@ async def install_custom_node(request): if not is_allowed_security_level(risky_level): print(SECURITY_MESSAGE_GENERAL) - return web.Response(status=404) + return web.Response(status=404, text="A security error has occurred. Please check the terminal logs") node_spec = core.unified_manager.resolve_node_spec(node_spec_str) if node_spec is None: - return + return web.Response(status=400, text=f"Cannot resolve install target: '{node_spec_str}'") node_name, version_spec, is_specified = node_spec res = await core.unified_manager.install_by_id(node_name, version_spec, json_data['channel'], json_data['mode'], return_postinstall=skip_post_install) # discard post install if skip_post_install mode - if res not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']: - return web.Response(status=400) + if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']: + return web.Response(status=400, text=f"Installation failed: {res}") - return web.Response(status=200) + return web.Response(status=200, text="Installation success.") @routes.post("/customnode/fix") async def fix_custom_node(request): if not is_allowed_security_level('middle'): print(SECURITY_MESSAGE_GENERAL) - return web.Response(status=403) + return web.Response(status=403, text="A security error has occurred. Please check the terminal logs") json_data = await request.json() @@ -924,7 +908,7 @@ async def fix_custom_node(request): return web.json_response({}, content_type='application/json') print(f"ERROR: An error occurred while fixing '{node_name}@{node_ver}'.") - return web.Response(status=400) + return web.Response(status=400, text=f"An error occurred while fixing '{node_name}@{node_ver}'.") @routes.post("/customnode/install/git_url") @@ -963,7 +947,7 @@ async def install_custom_node_pip(request): async def uninstall_custom_node(request): if not is_allowed_security_level('middle'): print(SECURITY_MESSAGE_MIDDLE_OR_BELOW) - return web.Response(status=403) + return web.Response(status=403, text="A security error has occurred. Please check the terminal logs") json_data = await request.json() @@ -983,14 +967,14 @@ async def uninstall_custom_node(request): return web.json_response({}, content_type='application/json') print(f"ERROR: An error occurred while uninstalling '{node_name}'.") - return web.Response(status=400) + return web.Response(status=400, text=f"An error occurred while uninstalling '{node_name}'.") @routes.post("/customnode/update") async def update_custom_node(request): if not is_allowed_security_level('middle'): print(SECURITY_MESSAGE_MIDDLE_OR_BELOW) - return web.Response(status=403) + return web.Response(status=403, text="A security error has occurred. Please check the terminal logs") json_data = await request.json() @@ -1003,14 +987,14 @@ async def update_custom_node(request): res = core.unified_manager.unified_update(node_name, json_data['version']) - core.clear_pip_cache() + manager_util.clear_pip_cache() if res.result: print("After restarting ComfyUI, please refresh the browser.") return web.json_response({}, content_type='application/json') print(f"ERROR: An error occurred while updating '{node_name}'.") - return web.Response(status=400) + return web.Response(status=400, text=f"An error occurred while updating '{node_name}'.") @routes.get("/comfyui_manager/update_comfyui") @@ -1075,7 +1059,7 @@ async def disable_node(request): if res: return web.json_response({}, content_type='application/json') - return web.Response(status=400) + return web.Response(status=400, text="Failed to disable") @routes.get("/manager/migrate_unmanaged_nodes") @@ -1123,7 +1107,7 @@ async def install_model(request): 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) + model_dir = get_model_dir(json_data, True) download_url(model_url, model_dir, filename=json_data['filename']) if model_path.endswith('.zip'): res = core.unzip(model_path) @@ -1147,7 +1131,7 @@ async def install_model(request): return web.Response(status=400) -@PromptServer.instance.routes.get("/manager/preview_method") +@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']) @@ -1306,14 +1290,9 @@ def restart(self): sys_argv.remove('--windows-standalone-build') if sys.platform.startswith('win32'): - return os.execv(sys.executable, ['"' + sys.executable + '"', '"' + sys.argv[0] + '"'] + sys.argv[1:]) + return os.execv(sys.executable, ['"' + sys.executable + '"', '"' + sys_argv[0] + '"'] + sys_argv[1:]) else: - 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 + return os.execv(sys.executable, [sys.executable] + sys_argv) @routes.post("/manager/component/save") @@ -1327,9 +1306,9 @@ async def save_component(request): os.mkdir(components_path) if 'packname' in workflow and workflow['packname'] != '': - sanitized_name = sanitize_filename(workflow['packname']) + '.pack' + sanitized_name = manager_util.sanitize_filename(workflow['packname']) + '.pack' else: - sanitized_name = sanitize_filename(name) + '.json' + sanitized_name = manager_util.sanitize_filename(name) + '.json' filepath = os.path.join(components_path, sanitized_name) components = {} @@ -1368,10 +1347,6 @@ async def load_components(request): return web.Response(status=400) -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') diff --git a/glob/manager_util.py b/glob/manager_util.py index 92ce534d..395391a9 100644 --- a/glob/manager_util.py +++ b/glob/manager_util.py @@ -5,7 +5,7 @@ import os from datetime import datetime import subprocess import sys - +import re cache_lock = threading.Lock() @@ -140,27 +140,6 @@ def sanitize_tag(x): return x.replace('<', '<').replace('>', '>') -def download_url(url, dest_folder, filename): - import requests - - # Ensure the destination folder exists - if not os.path.exists(dest_folder): - os.makedirs(dest_folder) - - # Full path to save the file - dest_path = os.path.join(dest_folder, filename) - - # Download the file - response = requests.get(url, stream=True) - if response.status_code == 200: - with open(dest_path, 'wb') as file: - for chunk in response.iter_content(chunk_size=1024): - if chunk: - file.write(chunk) - else: - raise Exception(f"Failed to download file from {url}") - - def extract_package_as_zip(file_path, extract_path): import zipfile try: @@ -172,8 +151,11 @@ def extract_package_as_zip(file_path, extract_path): except zipfile.BadZipFile: print(f"File '{file_path}' is not a zip or is corrupted.") return None + + pip_map = None + def get_installed_packages(renew=False): global pip_map @@ -318,3 +300,12 @@ class PIPFixer: except Exception as e: print("[manager-core] Failed to restore numpy") print(e) + + +def sanitize(data): + return data.replace("<", "<").replace(">", ">") + + +def sanitize_filename(input_string): + result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string) + return result_string