added: /v2/snapshot/diff

modified: use 'packaging.version` instead custom StrictVersion
This commit is contained in:
Dr.Lt.Data 2025-07-29 23:15:11 +09:00
parent 5316ec1b4d
commit 62da330182
6 changed files with 316 additions and 46 deletions

View File

@ -15,7 +15,7 @@ import re
import logging import logging
import platform import platform
import shlex import shlex
from packaging import version
cache_lock = threading.Lock() cache_lock = threading.Lock()
session_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.") # print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.")
class StrictVersion: class StrictVersion:
def __init__(self, version_string): def __init__(self, version_string):
self.obj = version.parse(version_string)
self.version_string = version_string self.version_string = version_string
self.major = 0 self.major = self.obj.major
self.minor = 0 self.minor = self.obj.minor
self.patch = 0 self.patch = self.obj.micro
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]
def __str__(self): def __str__(self):
version = f"{self.major}.{self.minor}.{self.patch}" return self.version_string
if self.pre_release:
version += f"-{self.pre_release}"
return version
def __eq__(self, other): def __eq__(self, other):
return (self.major, self.minor, self.patch, self.pre_release) == \ return self.obj == other.obj
(other.major, other.minor, other.patch, other.pre_release)
def __lt__(self, other): def __lt__(self, other):
if (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch): return self.obj < other.obj
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
def __le__(self, other): def __le__(self, other):
return self == other or self < other return self.obj == other.obj or self.obj < other.obj
def __gt__(self, other): def __gt__(self, other):
return not self <= other return not self.obj <= other.obj
def __ge__(self, other): def __ge__(self, other):
return not self < other return not self.obj < other.obj
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self.obj == other.obj
def simple_hash(input_string): def simple_hash(input_string):

View File

@ -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}

View File

@ -2646,8 +2646,8 @@ async def get_current_snapshot(custom_nodes_only = False):
commit_hash = git_utils.get_commit_hash(fullpath) commit_hash = git_utils.get_commit_hash(fullpath)
url = git_utils.git_url(fullpath) url = git_utils.git_url(fullpath)
git_custom_nodes[url] = dict(hash=commit_hash, disabled=is_disabled) git_custom_nodes[url] = dict(hash=commit_hash, disabled=is_disabled)
except Exception: except Exception as e:
print(f"Failed to extract snapshots for the custom node '{path}'.") print(f"Failed to extract snapshots for the custom node '{path}'. / {e}")
elif path.endswith('.py'): elif path.endswith('.py'):
is_disabled = path.endswith(".py.disabled") is_disabled = path.endswith(".py.disabled")

View File

@ -47,7 +47,7 @@ from ..common import manager_util
from ..common import cm_global from ..common import cm_global
from ..common import manager_downloader from ..common import manager_downloader
from ..common import context from ..common import context
from ..common import snapshot_util
from ..data_models import ( from ..data_models import (
@ -1593,6 +1593,46 @@ async def save_snapshot(request):
return web.Response(status=400) 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): def unzip_install(files):
temp_filename = "manager-temp.zip" temp_filename = "manager-temp.zip"
for url in files: for url in files:

View File

@ -24,6 +24,7 @@ from ..common import cm_global
from ..common import manager_downloader from ..common import manager_downloader
from ..common import context from ..common import context
from ..common import manager_security from ..common import manager_security
from ..common import snapshot_util
logging.info(f"### Loading: ComfyUI-Manager ({core.version_str})") 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') 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): async def get_snapshot_list(request):
items = [f[:-5] for f in os.listdir(context.manager_snapshot_path) if f.endswith('.json')] items = [f[:-5] for f in os.listdir(context.manager_snapshot_path) if f.endswith('.json')]
items.sort(reverse=True) items.sort(reverse=True)
@ -1236,6 +1237,46 @@ async def save_snapshot(request):
return web.Response(status=400) 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): def unzip_install(files):
temp_filename = 'manager-temp.zip' temp_filename = 'manager-temp.zip'
for url in files: for url in files:

View File

@ -1209,6 +1209,89 @@ paths:
description: Snapshot saved successfully description: Snapshot saved successfully
'400': '400':
description: Error saving snapshot 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) # ComfyUI Management Endpoints (v2)
/v2/comfyui_manager/comfyui_versions: /v2/comfyui_manager/comfyui_versions:
get: get: