From 62da3301822f69fd28536892c05d16792e60daee Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Tue, 29 Jul 2025 23:15:11 +0900 Subject: [PATCH 1/2] added: /v2/snapshot/diff modified: use 'packaging.version` instead custom StrictVersion --- comfyui_manager/common/manager_util.py | 54 ++------- comfyui_manager/common/snapshot_util.py | 136 +++++++++++++++++++++++ comfyui_manager/glob/manager_core.py | 4 +- comfyui_manager/glob/manager_server.py | 42 ++++++- comfyui_manager/legacy/manager_server.py | 43 ++++++- openapi.yaml | 83 ++++++++++++++ 6 files changed, 316 insertions(+), 46 deletions(-) create mode 100644 comfyui_manager/common/snapshot_util.py diff --git a/comfyui_manager/common/manager_util.py b/comfyui_manager/common/manager_util.py index 8cd7c2110..6edb1a284 100644 --- a/comfyui_manager/common/manager_util.py +++ b/comfyui_manager/common/manager_util.py @@ -15,7 +15,7 @@ import logging import platform import shlex - +from packaging import version cache_lock = threading.Lock() session_lock = threading.Lock() @@ -58,62 +58,32 @@ def make_pip_cmd(cmd): # print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.") class StrictVersion: def __init__(self, version_string): + self.obj = version.parse(version_string) self.version_string = version_string - self.major = 0 - self.minor = 0 - self.patch = 0 - self.pre_release = None - self.parse_version_string() - - def parse_version_string(self): - parts = self.version_string.split('.') - if not parts: - raise ValueError("Version string must not be empty") - - self.major = int(parts[0]) - self.minor = int(parts[1]) if len(parts) > 1 else 0 - self.patch = int(parts[2]) if len(parts) > 2 else 0 - - # Handling pre-release versions if present - if len(parts) > 3: - self.pre_release = parts[3] + self.major = self.obj.major + self.minor = self.obj.minor + self.patch = self.obj.micro def __str__(self): - version = f"{self.major}.{self.minor}.{self.patch}" - if self.pre_release: - version += f"-{self.pre_release}" - return version + return self.version_string def __eq__(self, other): - return (self.major, self.minor, self.patch, self.pre_release) == \ - (other.major, other.minor, other.patch, other.pre_release) + return self.obj == other.obj def __lt__(self, other): - if (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch): - return self.pre_release_compare(self.pre_release, other.pre_release) < 0 - return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch) - - @staticmethod - def pre_release_compare(pre1, pre2): - if pre1 == pre2: - return 0 - if pre1 is None: - return 1 - if pre2 is None: - return -1 - return -1 if pre1 < pre2 else 1 + return self.obj < other.obj def __le__(self, other): - return self == other or self < other + return self.obj == other.obj or self.obj < other.obj def __gt__(self, other): - return not self <= other + return not self.obj <= other.obj def __ge__(self, other): - return not self < other + return not self.obj < other.obj def __ne__(self, other): - return not self == other + return not self.obj == other.obj def simple_hash(input_string): diff --git a/comfyui_manager/common/snapshot_util.py b/comfyui_manager/common/snapshot_util.py new file mode 100644 index 000000000..b3653fe78 --- /dev/null +++ b/comfyui_manager/common/snapshot_util.py @@ -0,0 +1,136 @@ +from . import manager_util +from . import git_utils +import json +import yaml +import logging + +def read_snapshot(snapshot_path): + try: + + with open(snapshot_path, 'r', encoding="UTF-8") as snapshot_file: + if snapshot_path.endswith('.json'): + info = json.load(snapshot_file) + elif snapshot_path.endswith('.yaml'): + info = yaml.load(snapshot_file, Loader=yaml.SafeLoader) + info = info['custom_nodes'] + + return info + except Exception as e: + logging.warning(f"Failed to read snapshot file: {snapshot_path}\nError: {e}") + + return None + + +def diff_snapshot(a, b): + if not a or not b: + return None + + nodepack_diff = { + 'added': {}, + 'removed': [], + 'upgraded': {}, + 'downgraded': {}, + 'changed': [] + } + + pip_diff = { + 'added': {}, + 'upgraded': {}, + 'downgraded': {} + } + + # check: comfyui + if a.get('comfyui_version') != b.get('comfyui_version'): + nodepack_diff['changed'].append('comfyui') + + # check: cnr nodes + a_cnrs = a.get('cnr_custom_nodes', {}) + b_cnrs = b.get('cnr_custom_nodes', {}) + + if 'comfyui-manager' in a_cnrs: + del a_cnrs['comfyui-manager'] + if 'comfyui-manager' in b_cnrs: + del b_cnrs['comfyui-manager'] + + for k, v in a_cnrs.items(): + if k not in b_cnrs.keys(): + nodepack_diff['removed'].append(k) + elif a_cnrs[k] != b_cnrs[k]: + a_ver = manager_util.StrictVersion(a_cnrs[k]) + b_ver = manager_util.StrictVersion(b_cnrs[k]) + if a_ver < b_ver: + nodepack_diff['upgraded'][k] = {'from': a_cnrs[k], 'to': b_cnrs[k]} + elif a_ver > b_ver: + nodepack_diff['downgraded'][k] = {'from': a_cnrs[k], 'to': b_cnrs[k]} + + added_cnrs = set(b_cnrs.keys()) - set(a_cnrs.keys()) + for k in added_cnrs: + nodepack_diff['added'][k] = b_cnrs[k] + + # check: git custom nodes + a_gits = a.get('git_custom_nodes', {}) + b_gits = b.get('git_custom_nodes', {}) + + a_gits = {git_utils.normalize_url(k): v for k, v in a_gits.items() if k.lower() != 'comfyui-manager'} + b_gits = {git_utils.normalize_url(k): v for k, v in b_gits.items() if k.lower() != 'comfyui-manager'} + + for k, v in a_gits.items(): + if k not in b_gits.keys(): + nodepack_diff['removed'].append(k) + elif not v['disabled'] and b_gits[k]['disabled']: + nodepack_diff['removed'].append(k) + elif v['disabled'] and not b_gits[k]['disabled']: + nodepack_diff['added'].append(k) + elif v['hash'] != b_gits[k]['hash']: + a_date = v.get('commit_timestamp') + b_date = b_gits[k].get('commit_timestamp') + if a_date is not None and b_date is not None: + if a_date < b_date: + nodepack_diff['upgraded'].append(k) + elif a_date > b_date: + nodepack_diff['downgraded'].append(k) + else: + nodepack_diff['changed'].append(k) + + # check: pip packages + a_pip = a.get('pips', {}) + b_pip = b.get('pips', {}) + for k, v in a_pip.items(): + if '==' in k: + package_name, version = k.split('==', 1) + else: + package_name, version = k, None + + for k2, v2 in b_pip.items(): + if '==' in k2: + package_name2, version2 = k2.split('==', 1) + else: + package_name2, version2 = k2, None + + if package_name.lower() == package_name2.lower(): + if version != version2: + a_ver = manager_util.StrictVersion(version) if version else None + b_ver = manager_util.StrictVersion(version2) if version2 else None + if a_ver and b_ver: + if a_ver < b_ver: + pip_diff['upgraded'][package_name] = {'from': version, 'to': version2} + elif a_ver > b_ver: + pip_diff['downgraded'][package_name] = {'from': version, 'to': version2} + elif not a_ver and b_ver: + pip_diff['added'][package_name] = version2 + + a_pip_names = {k.split('==', 1)[0].lower() for k in a_pip.keys()} + + for k in b_pip.keys(): + if '==' in k: + package_name = k.split('==', 1)[0] + package_version = k.split('==', 1)[1] + else: + package_name = k + package_version = None + + if package_name.lower() not in a_pip_names: + if package_version: + pip_diff['added'][package_name] = package_version + + return {'nodepack_diff': nodepack_diff, 'pip_diff': pip_diff} diff --git a/comfyui_manager/glob/manager_core.py b/comfyui_manager/glob/manager_core.py index e8bb5823e..415ed470e 100644 --- a/comfyui_manager/glob/manager_core.py +++ b/comfyui_manager/glob/manager_core.py @@ -2646,8 +2646,8 @@ async def get_current_snapshot(custom_nodes_only = False): commit_hash = git_utils.get_commit_hash(fullpath) url = git_utils.git_url(fullpath) git_custom_nodes[url] = dict(hash=commit_hash, disabled=is_disabled) - except Exception: - print(f"Failed to extract snapshots for the custom node '{path}'.") + except Exception as e: + print(f"Failed to extract snapshots for the custom node '{path}'. / {e}") elif path.endswith('.py'): is_disabled = path.endswith(".py.disabled") diff --git a/comfyui_manager/glob/manager_server.py b/comfyui_manager/glob/manager_server.py index ff7db2bd0..18a1942d8 100644 --- a/comfyui_manager/glob/manager_server.py +++ b/comfyui_manager/glob/manager_server.py @@ -47,7 +47,7 @@ from ..common import cm_global from ..common import manager_downloader from ..common import context - +from ..common import snapshot_util from ..data_models import ( @@ -1593,6 +1593,46 @@ async def save_snapshot(request): return web.Response(status=400) +@routes.get("/v2/snapshot/diff") +async def get_snapshot_diff(request): + try: + from_id = request.rel_url.query.get("from") + to_id = request.rel_url.query.get("to") + + if (from_id is not None and '..' in from_id) or (to_id is not None and '..' in to_id): + logging.error("/v2/snapshot/diff: invalid 'from' or 'to' parameter.") + return web.Response(status=400) + + if from_id is None: + from_json = await core.get_current_snapshot() + else: + from_path = os.path.join(context.manager_snapshot_path, f"{from_id}.json") + if not os.path.exists(from_path): + logging.error(f"/v2/snapshot/diff: 'from' parameter file not found: {from_path}") + return web.Response(status=400) + + from_json = snapshot_util.read_snapshot(from_path) + + if to_id is None: + logging.error("/v2/snapshot/diff: 'to' parameter is required.") + return web.Response(status=401) + else: + to_path = os.path.join(context.manager_snapshot_path, f"{to_id}.json") + if not os.path.exists(to_path): + logging.error(f"/v2/snapshot/diff: 'to' parameter file not found: {to_path}") + return web.Response(status=400) + + to_json = snapshot_util.read_snapshot(to_path) + + return web.json_response(snapshot_util.diff_snapshot(from_json, to_json), content_type='application/json') + + except Exception as e: + logging.error(f"[ComfyUI-Manager] Error in /v2/snapshot/diff: {e}") + traceback.print_exc() + # Return a generic error response + return web.Response(status=400) + + def unzip_install(files): temp_filename = "manager-temp.zip" for url in files: diff --git a/comfyui_manager/legacy/manager_server.py b/comfyui_manager/legacy/manager_server.py index c873aca2a..394ff9909 100644 --- a/comfyui_manager/legacy/manager_server.py +++ b/comfyui_manager/legacy/manager_server.py @@ -24,6 +24,7 @@ from ..common import manager_downloader from ..common import context from ..common import manager_security +from ..common import snapshot_util logging.info(f"### Loading: ComfyUI-Manager ({core.version_str})") @@ -1168,7 +1169,7 @@ async def fetch_externalmodel_list(request): return web.json_response(json_obj, content_type='application/json') -@PromptServer.instance.routes.get("/v2/snapshot/getlist") +@routes.get("/v2/snapshot/getlist") async def get_snapshot_list(request): items = [f[:-5] for f in os.listdir(context.manager_snapshot_path) if f.endswith('.json')] items.sort(reverse=True) @@ -1236,6 +1237,46 @@ async def save_snapshot(request): return web.Response(status=400) +@routes.get("/v2/snapshot/diff") +async def get_snapshot_diff(request): + try: + from_id = request.rel_url.query.get("from") + to_id = request.rel_url.query.get("to") + + if (from_id is not None and '..' in from_id) or (to_id is not None and '..' in to_id): + logging.error("/v2/snapshot/diff: invalid 'from' or 'to' parameter.") + return web.Response(status=400) + + if from_id is None: + from_json = await core.get_current_snapshot() + else: + from_path = os.path.join(context.manager_snapshot_path, f"{from_id}.json") + if not os.path.exists(from_path): + logging.error(f"/v2/snapshot/diff: 'from' parameter file not found: {from_path}") + return web.Response(status=400) + + from_json = snapshot_util.read_snapshot(from_path) + + if to_id is None: + logging.error("/v2/snapshot/diff: 'to' parameter is required.") + return web.Response(status=401) + else: + to_path = os.path.join(context.manager_snapshot_path, f"{to_id}.json") + if not os.path.exists(to_path): + logging.error(f"/v2/snapshot/diff: 'to' parameter file not found: {to_path}") + return web.Response(status=400) + + to_json = snapshot_util.read_snapshot(to_path) + + return web.json_response(snapshot_util.diff_snapshot(from_json, to_json), content_type='application/json') + + except Exception as e: + logging.error(f"[ComfyUI-Manager] Error in /v2/snapshot/diff: {e}") + traceback.print_exc() + # Return a generic error response + return web.Response(status=400) + + def unzip_install(files): temp_filename = 'manager-temp.zip' for url in files: diff --git a/openapi.yaml b/openapi.yaml index 0e17dc74e..5a6c2e4f0 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1209,6 +1209,89 @@ paths: description: Snapshot saved successfully '400': description: Error saving snapshot + /v2/snapshot/diff: + get: + summary: Get snapshot diff + description: Returns the changes that would occur when restoring from the 'from' snapshot to the 'to' snapshot. + parameters: + - name: from + in: query + required: false + description: This parameter refers to the existing snapshot; if omitted, it defaults to the current snapshot. + schema: + type: string + - name: to + in: query + required: true + description: This parameter is the snapshot to compare against the existing snapshot. + schema: + type: string + responses: + '200': + description: Successful operation + content: + application/json: + schema: + type: object + properties: + nodepack_diff: + type: object + properties: + added: + type: object + additionalProperties: + type: string + removed: + type: array + items: + type: string + upgraded: + type: object + additionalProperties: + type: object + properties: + from: + type: string + to: + type: string + downgraded: + type: object + additionalProperties: + type: object + properties: + from: + type: string + to: + type: string + changed: + type: array + items: + type: string + pip_diff: + type: object + properties: + added: + type: object + additionalProperties: + type: string + upgraded: + type: object + additionalProperties: + type: object + properties: + from: + type: string + to: + type: string + downgraded: + type: object + additionalProperties: + type: object + properties: + from: + type: string + to: + type: string # ComfyUI Management Endpoints (v2) /v2/comfyui_manager/comfyui_versions: get: From 24ca0ab538a9842ad19f3bf2a67c00cb9475dd73 Mon Sep 17 00:00:00 2001 From: "Dr.Lt.Data" Date: Tue, 29 Jul 2025 23:22:19 +0900 Subject: [PATCH 2/2] fix: Issue where the ComfyUI hash difference was not appearing in `v2/snapshot/diff`. --- comfyui_manager/common/snapshot_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_manager/common/snapshot_util.py b/comfyui_manager/common/snapshot_util.py index b3653fe78..f3f77a0c7 100644 --- a/comfyui_manager/common/snapshot_util.py +++ b/comfyui_manager/common/snapshot_util.py @@ -40,9 +40,9 @@ def diff_snapshot(a, b): } # check: comfyui - if a.get('comfyui_version') != b.get('comfyui_version'): + if a.get('comfyui') != b.get('comfyui'): nodepack_diff['changed'].append('comfyui') - + # check: cnr nodes a_cnrs = a.get('cnr_custom_nodes', {}) b_cnrs = b.get('cnr_custom_nodes', {})