refactor: remove package-level caching to support dynamic installation

Remove package-level caching in cnr_utils and node_package modules to enable
proper dynamic custom node installation and version switching without ComfyUI
server restarts.

Key Changes:
- Remove @lru_cache decorators from version-sensitive functions
- Remove cached_property from NodePackage for dynamic state updates
- Add comprehensive test suite with parallel execution support
- Implement version switching tests (CNR ↔ Nightly)
- Add case sensitivity integration tests
- Improve error handling and logging

API Priority Rules (manager_core.py:1801):
- Enabled-Priority: Show only enabled version when both exist
- CNR-Priority: Show only CNR when both CNR and Nightly are disabled
- Prevents duplicate package entries in /v2/customnode/installed API
- Cross-match using cnr_id and aux_id for CNR ↔ Nightly detection

Test Infrastructure:
- 8 test files with 59 comprehensive test cases
- Parallel test execution across 5 isolated environments
- Automated test scripts with environment setup
- Configurable timeout (60 minutes default)
- Support for both master and dr-support-pip-cm branches

Bug Fixes:
- Fix COMFYUI_CUSTOM_NODES_PATH environment variable export
- Resolve test fixture regression with module-level variables
- Fix import timing issues in test configuration
- Register pytest integration marker to eliminate warnings
- Fix POSIX compliance in shell scripts (((var++)) → $((var + 1)))

Documentation:
- CNR_VERSION_MANAGEMENT_DESIGN.md v1.0 → v1.1 with API priority rules
- Add test guides and execution documentation (TESTING_PROMPT.md)
- Add security-enhanced installation guide
- Create CLI migration guides and references
- Document package version management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dr.Lt.Data
2025-11-07 10:04:21 +09:00
parent d3906e3cbc
commit 43647249cf
62 changed files with 17790 additions and 10789 deletions

View File

@@ -36,9 +36,9 @@ if not os.path.exists(os.path.join(comfy_path, 'folder_paths.py')):
import utils.extra_config
from ..common import cm_global
from ..legacy import manager_core as core
from ..glob import manager_core as core
from ..common import context
from ..legacy.manager_core import unified_manager
from ..glob.manager_core import unified_manager
from ..common import cnr_utils
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
@@ -129,8 +129,7 @@ class Ctx:
if channel is not None:
self.channel = channel
asyncio.run(unified_manager.reload(cache_mode=self.mode, dont_wait=False))
asyncio.run(unified_manager.load_nightly(self.channel, self.mode))
unified_manager.reload()
def set_no_deps(self, no_deps):
self.no_deps = no_deps
@@ -188,9 +187,14 @@ def install_node(node_spec_str, is_all=False, cnt_msg='', **kwargs):
exit_on_fail = kwargs.get('exit_on_fail', False)
print(f"install_node exit on fail:{exit_on_fail}...")
if core.is_valid_url(node_spec_str):
# install via urls
res = asyncio.run(core.gitclone_install(node_spec_str, no_deps=cmd_ctx.no_deps))
if unified_manager.is_url_like(node_spec_str):
# install via git URLs
repo_name = os.path.basename(node_spec_str)
if repo_name.endswith('.git'):
repo_name = repo_name[:-4]
res = asyncio.run(unified_manager.repo_install(
node_spec_str, repo_name, instant_execution=True, no_deps=cmd_ctx.no_deps
))
if not res.result:
print(res.msg)
print(f"[bold red]ERROR: An error occurred while installing '{node_spec_str}'.[/bold red]")
@@ -224,7 +228,7 @@ def install_node(node_spec_str, is_all=False, cnt_msg='', **kwargs):
print(f"{cnt_msg} [INSTALLED] {node_name:50}[{res.target}]")
elif res.action == 'switch-cnr' and res.result:
print(f"{cnt_msg} [INSTALLED] {node_name:50}[{res.target}]")
elif (res.action == 'switch-cnr' or res.action == 'install-cnr') and not res.result and node_name in unified_manager.cnr_map:
elif (res.action == 'switch-cnr' or res.action == 'install-cnr') and not res.result and cnr_utils.get_nodepack(node_name):
print(f"\nAvailable version of '{node_name}'")
show_versions(node_name)
print("")
@@ -315,10 +319,10 @@ def update_parallel(nodes):
if 'all' in nodes:
is_all = True
nodes = []
for x in unified_manager.active_nodes.keys():
nodes.append(x)
for x in unified_manager.unknown_active_nodes.keys():
nodes.append(x+"@unknown")
for packages in unified_manager.installed_node_packages.values():
for pack in packages:
if pack.is_enabled:
nodes.append(pack.id)
else:
nodes = [x for x in nodes if x.lower() not in ['comfy', 'comfyui']]
@@ -416,121 +420,60 @@ def disable_node(node_spec_str: str, is_all=False, cnt_msg=''):
def show_list(kind, simple=False):
custom_nodes = asyncio.run(unified_manager.get_custom_nodes(channel=cmd_ctx.channel, mode=cmd_ctx.mode))
"""
Show installed nodepacks only with on-demand metadata retrieval
Supported kinds: 'installed', 'enabled', 'disabled'
"""
# Validate supported commands
if kind not in ['installed', 'enabled', 'disabled']:
print(f"[bold red]Unsupported: 'show {kind}'. Available options: installed/enabled/disabled[/bold red]")
print("Note: 'show all', 'show not-installed', and 'show cnr' are no longer supported.")
print("Use 'show installed' to see all installed packages.")
return
# collect not-installed unknown nodes
not_installed_unknown_nodes = []
repo_unknown = {}
# Get all installed packages from glob unified_manager
all_packages = []
for packages in unified_manager.installed_node_packages.values():
all_packages.extend(packages)
# Filter by status
if kind == 'enabled':
packages = [pkg for pkg in all_packages if pkg.is_enabled]
elif kind == 'disabled':
packages = [pkg for pkg in all_packages if pkg.is_disabled]
else: # 'installed'
packages = all_packages
for k, v in custom_nodes.items():
if 'cnr_latest' not in v:
if len(v['files']) == 1:
repo_url = v['files'][0]
node_name = repo_url.split('/')[-1]
if node_name not in unified_manager.unknown_inactive_nodes and node_name not in unified_manager.unknown_active_nodes:
not_installed_unknown_nodes.append(v)
else:
repo_unknown[node_name] = v
processed = {}
unknown_processed = []
flag = kind in ['all', 'cnr', 'installed', 'enabled']
for k, v in unified_manager.active_nodes.items():
if flag:
cnr = unified_manager.cnr_map.get(k)
if cnr:
processed[k] = "[ ENABLED ] ", cnr['name'], k, cnr['publisher']['name'], v[0]
else:
processed[k] = None
else:
processed[k] = None
if flag and kind != 'cnr':
for k, v in unified_manager.unknown_active_nodes.items():
item = repo_unknown.get(k)
if item is None:
continue
log_item = "[ ENABLED ] ", item['title'], k, item['author']
unknown_processed.append(log_item)
flag = kind in ['all', 'cnr', 'installed', 'disabled']
for k, v in unified_manager.cnr_inactive_nodes.items():
if k in processed:
continue
if flag:
cnr = unified_manager.cnr_map.get(k) # NOTE: can this be None if removed from CNR after installed
if cnr:
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], ", ".join(list(v.keys()))
else:
processed[k] = None
else:
processed[k] = None
for k, v in unified_manager.nightly_inactive_nodes.items():
if k in processed:
continue
if flag:
cnr = unified_manager.cnr_map.get(k)
if cnr:
processed[k] = "[ DISABLED ] ", cnr['name'], k, cnr['publisher']['name'], 'nightly'
else:
processed[k] = None
else:
processed[k] = None
if flag and kind != 'cnr':
for k, v in unified_manager.unknown_inactive_nodes.items():
item = repo_unknown.get(k)
if item is None:
continue
log_item = "[ DISABLED ] ", item['title'], k, item['author']
unknown_processed.append(log_item)
flag = kind in ['all', 'cnr', 'not-installed']
for k, v in unified_manager.cnr_map.items():
if k in processed:
continue
if flag:
cnr = unified_manager.cnr_map.get(k)
if cnr:
ver_spec = v['latest_version']['version'] if 'latest_version' in v else '0.0.0'
processed[k] = "[ NOT INSTALLED ] ", cnr['name'], k, cnr['publisher']['name'], ver_spec
else:
processed[k] = None
else:
processed[k] = None
if flag and kind != 'cnr':
for x in not_installed_unknown_nodes:
if len(x['files']) == 1:
node_id = os.path.basename(x['files'][0])
log_item = "[ NOT INSTALLED ] ", x['title'], node_id, x['author']
unknown_processed.append(log_item)
for x in processed.values():
if x is None:
continue
prefix, title, short_id, author, ver_spec = x
# Display packages
for package in sorted(packages, key=lambda x: x.id):
# Basic info from InstalledNodePackage
status = "[ ENABLED ]" if package.is_enabled else "[ DISABLED ]"
# Enhanced info with on-demand CNR retrieval
display_name = package.id
author = "Unknown"
version = package.version
# Try to get additional info from CNR for better display
if package.is_from_cnr:
try:
cnr_info = cnr_utils.get_nodepack(package.id)
if cnr_info:
display_name = cnr_info.get('name', package.id)
if 'publisher' in cnr_info and 'name' in cnr_info['publisher']:
author = cnr_info['publisher']['name']
except Exception:
# Fallback to basic info if CNR lookup fails
pass
elif package.is_nightly:
version = "nightly"
elif package.is_unknown:
version = "unknown"
if simple:
print(title+'@'+ver_spec)
print(f"{display_name}@{version}")
else:
print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) \\[{ver_spec}]")
for x in unknown_processed:
prefix, title, short_id, author = x
if simple:
print(title+'@unknown')
else:
print(f"{prefix} {title:50} {short_id:30} (author: {author:20}) [UNKNOWN]")
print(f"{status} {display_name:50} {package.id:30} (author: {author:20}) [{version}]")
async def show_snapshot(simple_mode=False):
@@ -571,37 +514,14 @@ async def auto_save_snapshot():
def get_all_installed_node_specs():
"""
Get all installed node specifications using glob InstalledNodePackage data structure
"""
res = []
processed = set()
for k, v in unified_manager.active_nodes.items():
node_spec_str = f"{k}@{v[0]}"
res.append(node_spec_str)
processed.add(k)
for k in unified_manager.cnr_inactive_nodes.keys():
if k in processed:
continue
latest = unified_manager.get_from_cnr_inactive_nodes(k)
if latest is not None:
node_spec_str = f"{k}@{str(latest[0])}"
for packages in unified_manager.installed_node_packages.values():
for pack in packages:
node_spec_str = f"{pack.id}@{pack.version}"
res.append(node_spec_str)
for k in unified_manager.nightly_inactive_nodes.keys():
if k in processed:
continue
node_spec_str = f"{k}@nightly"
res.append(node_spec_str)
for k in unified_manager.unknown_active_nodes.keys():
node_spec_str = f"{k}@unknown"
res.append(node_spec_str)
for k in unified_manager.unknown_inactive_nodes.keys():
node_spec_str = f"{k}@unknown"
res.append(node_spec_str)
return res
@@ -1277,19 +1197,21 @@ def export_custom_node_ids(
cmd_ctx.set_channel_mode(channel, mode)
with open(path, "w", encoding='utf-8') as output_file:
for x in unified_manager.cnr_map.keys():
print(x, file=output_file)
# Export CNR package IDs using cnr_utils
try:
all_cnr = cnr_utils.get_all_nodepackages()
for package_id in all_cnr.keys():
print(package_id, file=output_file)
except Exception:
# If CNR lookup fails, continue with installed packages
pass
custom_nodes = asyncio.run(unified_manager.get_custom_nodes(channel=cmd_ctx.channel, mode=cmd_ctx.mode))
for x in custom_nodes.values():
if 'cnr_latest' not in x:
if len(x['files']) == 1:
repo_url = x['files'][0]
node_id = repo_url.split('/')[-1]
print(f"{node_id}@unknown", file=output_file)
if 'id' in x:
print(f"{x['id']}@unknown", file=output_file)
# Export installed packages that are not from CNR
for packages in unified_manager.installed_node_packages.values():
for pack in packages:
if pack.is_unknown or pack.is_nightly:
version_suffix = "@unknown" if pack.is_unknown else "@nightly"
print(f"{pack.id}{version_suffix}", file=output_file)
def main():

View File

@@ -34,6 +34,11 @@ variables = {}
APIs = {}
pip_overrides = {}
pip_blacklist = {}
pip_downgrade_blacklist = {}
def register_api(k, f):
global APIs
APIs[k] = f

View File

@@ -12,6 +12,10 @@ from . import manager_util
import requests
import toml
import logging
from . import git_utils
from cachetools import TTLCache, cached
query_ttl_cache = TTLCache(maxsize=100, ttl=60)
base_url = "https://api.comfy.org"
@@ -20,6 +24,29 @@ lock = asyncio.Lock()
is_cache_loading = False
def normalize_package_name(name: str) -> str:
"""
Normalize package name for case-insensitive matching.
This follows the same normalization pattern used throughout CNR:
- Strip leading/trailing whitespace
- Convert to lowercase
Args:
name: Package name to normalize (e.g., "ComfyUI_SigmoidOffsetScheduler" or " NodeName ")
Returns:
Normalized package name (e.g., "comfyui_sigmoidoffsetscheduler")
Examples:
>>> normalize_package_name("ComfyUI_SigmoidOffsetScheduler")
"comfyui_sigmoidoffsetscheduler"
>>> normalize_package_name(" NodeName ")
"nodename"
"""
return name.strip().lower()
async def get_cnr_data(cache_mode=True, dont_wait=True):
try:
return await _get_cnr_data(cache_mode, dont_wait)
@@ -37,7 +64,6 @@ async def _get_cnr_data(cache_mode=True, dont_wait=True):
page = 1
full_nodes = {}
# Determine form factor based on environment and platform
is_desktop = bool(os.environ.get('__COMFYUI_DESKTOP_VERSION__'))
@@ -138,7 +164,7 @@ def map_node_version(api_node_version):
Maps node version data from API response to NodeVersion dataclass.
Args:
api_data (dict): The 'node_version' part of the API response.
api_node_version (dict): The 'node_version' part of the API response.
Returns:
NodeVersion: An instance of NodeVersion dataclass populated with data from the API.
@@ -189,6 +215,80 @@ def install_node(node_id, version=None):
return None
@cached(query_ttl_cache)
def get_nodepack(packname):
"""
Retrieves the nodepack
Args:
packname (str): The unique identifier of the node.
Returns:
nodepack info {id, latest_version}
"""
url = f"{base_url}/nodes/{packname}"
response = requests.get(url, verify=not manager_util.bypass_ssl)
if response.status_code == 200:
info = response.json()
res = {
'id': info['id']
}
if 'latest_version' in info:
res['latest_version'] = info['latest_version']['version']
if 'repository' in info:
res['repository'] = info['repository']
return res
else:
return None
@cached(query_ttl_cache)
def get_nodepack_by_url(url):
"""
Retrieves the nodepack info for installation.
Args:
url (str): The unique identifier of the node.
Returns:
NodeVersion: Node version data or error message.
"""
# example query: https://api.comfy.org/nodes/search?repository_url_search=ltdrdata/ComfyUI-Impact-Pack&limit=1
url = f"nodes/search?repository_url_search={url}&limit=1"
response = requests.get(url, verify=not manager_util.bypass_ssl)
if response.status_code == 200:
# Convert the API response to a NodeVersion object
info = response.json().get('nodes', [])
if len(info) > 0:
info = info[0]
repo_url = info['repository']
if git_utils.compact_url(url) != git_utils.compact_url(repo_url):
return None
res = {
'id': info['id']
}
if 'latest_version' in info:
res['latest_version'] = info['latest_version']['version']
res['repository'] = info['repository']
return res
else:
return None
else:
return None
def all_versions_of_node(node_id):
url = f"{base_url}/nodes/{node_id}/versions?statuses=NodeVersionStatusActive&statuses=NodeVersionStatusPending"
@@ -211,8 +311,7 @@ def read_cnr_info(fullpath):
data = toml.load(f)
project = data.get('project', {})
name = project.get('name').strip().lower()
original_name = project.get('name')
name = project.get('name').strip()
# normalize version
# for example: 2.5 -> 2.5.0
@@ -224,7 +323,6 @@ def read_cnr_info(fullpath):
if name and version: # repository is optional
return {
"id": name,
"original_name": original_name,
"version": version,
"url": repository
}
@@ -254,4 +352,3 @@ def read_cnr_id(fullpath):
pass
return None

View File

@@ -77,6 +77,14 @@ def normalize_to_github_id(url) -> str:
return None
def compact_url(url):
github_id = normalize_to_github_id(url)
if github_id is not None:
return github_id
return url
def get_url_for_clone(url):
url = normalize_url(url)

View File

@@ -14,6 +14,7 @@ class InstalledNodePackage:
fullpath: str
disabled: bool
version: str
repo_url: str = None # Git repository URL for nightly packages
@property
def is_unknown(self) -> bool:
@@ -46,6 +47,8 @@ class InstalledNodePackage:
@staticmethod
def from_fullpath(fullpath: str, resolve_from_path) -> InstalledNodePackage:
from . import git_utils
parent_folder_name = os.path.basename(os.path.dirname(fullpath))
module_name = os.path.basename(fullpath)
@@ -54,6 +57,10 @@ class InstalledNodePackage:
disabled = True
elif parent_folder_name == ".disabled":
# Nodes under custom_nodes/.disabled/* are disabled
# Parse directory name format: packagename@version
# Examples:
# comfyui_sigmoidoffsetscheduler@nightly → id: comfyui_sigmoidoffsetscheduler, version: nightly
# comfyui_sigmoidoffsetscheduler@1_0_2 → id: comfyui_sigmoidoffsetscheduler, version: 1.0.2
node_id = module_name
disabled = True
else:
@@ -61,12 +68,35 @@ class InstalledNodePackage:
disabled = False
info = resolve_from_path(fullpath)
repo_url = None
version_from_dirname = None
# For disabled packages, try to extract version from directory name
if disabled and parent_folder_name == ".disabled" and '@' in module_name:
parts = module_name.split('@')
if len(parts) == 2:
node_id = parts[0] # Use the normalized name from directory
version_from_dirname = parts[1].replace('_', '.') # Convert 1_0_2 → 1.0.2
if info is None:
version = 'unknown'
version = version_from_dirname if version_from_dirname else 'unknown'
else:
node_id = info['id'] # robust module guessing
version = info['ver']
# Prefer version from directory name for disabled packages (preserves 'nightly' literal)
# Otherwise use version from package inspection (commit hash for git repos)
if version_from_dirname:
version = version_from_dirname
else:
version = info['ver']
# Get repository URL for both nightly and CNR packages
if version == 'nightly':
# For nightly packages, get repo URL from git
repo_url = git_utils.git_url(fullpath)
elif 'url' in info and info['url']:
# For CNR packages, get repo URL from pyproject.toml
repo_url = info['url']
return InstalledNodePackage(
id=node_id, fullpath=fullpath, disabled=disabled, version=version
id=node_id, fullpath=fullpath, disabled=disabled, version=version, repo_url=repo_url
)

View File

@@ -70,6 +70,7 @@ from .generated_models import (
InstallType,
SecurityLevel,
RiskLevel,
NetworkMode
)
__all__ = [
@@ -134,4 +135,5 @@ __all__ = [
"InstallType",
"SecurityLevel",
"RiskLevel",
"NetworkMode",
]

View File

@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: openapi.yaml
# timestamp: 2025-07-31T04:52:26+00:00
# timestamp: 2025-11-01T04:21:38+00:00
from __future__ import annotations
@@ -57,7 +57,12 @@ class ManagerPackInstalled(BaseModel):
description="The version of the pack that is installed (Git commit hash or semantic version)",
)
cnr_id: Optional[str] = Field(
None, description="The name of the pack if installed from the registry"
None,
description="The name of the pack if installed from the registry (normalized lowercase)",
)
original_name: Optional[str] = Field(
None,
description="The original case-preserved name of the pack from the registry",
)
aux_id: Optional[str] = Field(
None,
@@ -107,6 +112,12 @@ class SecurityLevel(str, Enum):
weak = "weak"
class NetworkMode(str, Enum):
public = "public"
private = "private"
offline = "offline"
class RiskLevel(str, Enum):
block = "block"
high_ = "high+"
@@ -155,8 +166,8 @@ class InstallPackParams(ManagerPackInfo):
description="GitHub repository URL (required if selected_version is nightly)",
)
pip: Optional[List[str]] = Field(None, description="PyPi dependency names")
mode: ManagerDatabaseSource
channel: ManagerChannel
mode: Optional[ManagerDatabaseSource] = None
channel: Optional[ManagerChannel] = None
skip_post_install: Optional[bool] = Field(
None, description="Whether to skip post-installation steps"
)
@@ -406,9 +417,7 @@ class ComfyUISystemState(BaseModel):
)
manager_version: Optional[str] = Field(None, description="ComfyUI Manager version")
security_level: Optional[SecurityLevel] = None
network_mode: Optional[str] = Field(
None, description="Network mode (online, offline, private)"
)
network_mode: Optional[NetworkMode] = None
cli_args: Optional[Dict[str, Any]] = Field(
None, description="Selected ComfyUI CLI arguments"
)
@@ -479,13 +488,13 @@ class QueueTaskItem(BaseModel):
params: Union[
InstallPackParams,
UpdatePackParams,
UpdateAllPacksParams,
UpdateComfyUIParams,
FixPackParams,
UninstallPackParams,
DisablePackParams,
EnablePackParams,
ModelMetadata,
UpdateComfyUIParams,
UpdateAllPacksParams,
]

View File

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@ import json
import logging
import os
import platform
import re
import shutil
import subprocess # don't remove this
import sys
@@ -26,7 +25,6 @@ from typing import Any, Optional
import folder_paths
import latent_preview
import nodes
from aiohttp import web
from comfy.cli_args import args
from pydantic import ValidationError
@@ -35,7 +33,6 @@ from comfyui_manager.glob.utils import (
formatting_utils,
model_utils,
security_utils,
node_pack_utils,
environment_utils,
)
@@ -47,6 +44,7 @@ from ..common import manager_util
from ..common import cm_global
from ..common import manager_downloader
from ..common import context
from ..common import cnr_utils
@@ -61,7 +59,6 @@ from ..data_models import (
ManagerMessageName,
BatchExecutionRecord,
ComfyUISystemState,
ImportFailInfoBulkRequest,
BatchOperation,
InstalledNodeInfo,
ComfyUIVersionInfo,
@@ -216,7 +213,7 @@ class TaskQueue:
history=self.get_history(),
running_queue=self.get_current_queue()[0],
pending_queue=self.get_current_queue()[1],
installed_packs=core.get_installed_node_packs(),
installed_packs=core.get_installed_nodepacks(),
)
@staticmethod
@@ -365,11 +362,7 @@ class TaskQueue:
item.kind,
)
# Force unified_manager to refresh its installed packages cache
await core.unified_manager.reload(
ManagerDatabaseSource.cache.value,
dont_wait=True,
update_cnr_map=False,
)
core.unified_manager.reload()
except Exception as e:
logging.warning(
f"[ComfyUI-Manager] Failed to refresh cache after {item.kind}: {e}"
@@ -619,7 +612,7 @@ class TaskQueue:
installed_nodes = {}
try:
node_packs = core.get_installed_node_packs()
node_packs = core.get_installed_nodepacks()
for pack_name, pack_info in node_packs.items():
# Determine install method and repository URL
install_method = "git" if pack_info.get("aux_id") else "cnr"
@@ -678,12 +671,12 @@ class TaskQueue:
level_str = config.get("security_level", "normal")
# Map the string to SecurityLevel enum
level_mapping = {
"strong": SecurityLevel.strong,
"normal": SecurityLevel.normal,
"normal-": SecurityLevel.normal_,
"weak": SecurityLevel.weak,
"strong": SecurityLevel.STRONG,
"normal": SecurityLevel.NORMAL,
"normal-": SecurityLevel.NORMAL_,
"weak": SecurityLevel.WEAK,
}
return level_mapping.get(level_str, SecurityLevel.normal)
return level_mapping.get(level_str, SecurityLevel.NORMAL)
except Exception:
return None
@@ -703,8 +696,6 @@ class TaskQueue:
cli_args["listen"] = args.listen
if hasattr(args, "port"):
cli_args["port"] = args.port
if hasattr(args, "preview_method"):
cli_args["preview_method"] = str(args.preview_method)
if hasattr(args, "enable_manager_legacy_ui"):
cli_args["enable_manager_legacy_ui"] = args.enable_manager_legacy_ui
if hasattr(args, "front_end_version"):
@@ -718,7 +709,7 @@ class TaskQueue:
def _get_custom_nodes_count(self) -> int:
"""Get total number of custom node packages."""
try:
node_packs = core.get_installed_node_packs()
node_packs = core.get_installed_nodepacks()
return len(node_packs)
except Exception:
return 0
@@ -818,24 +809,18 @@ class TaskQueue:
task_queue = TaskQueue()
# Preview method initialization
if args.preview_method == latent_preview.LatentPreviewMethod.NoPreviews:
environment_utils.set_preview_method(core.get_config()["preview_method"])
else:
logging.warning(
"[ComfyUI-Manager] Since --preview-method is set, ComfyUI-Manager's preview method feature will be ignored."
)
async def task_worker():
logging.debug("[ComfyUI-Manager] Task worker started")
await core.unified_manager.reload(ManagerDatabaseSource.cache.value)
core.unified_manager.reload()
async def do_install(params: InstallPackParams) -> str:
if not security_utils.is_allowed_security_level('middle+'):
logging.error(SECURITY_MESSAGE_MIDDLE_P)
return OperationResult.failed.value
# Note: For install, we use the original case as resolve_node_spec handles lookup
# Normalization is applied for uninstall, enable, disable operations
node_id = params.id
node_version = params.selected_version
channel = params.channel
@@ -891,7 +876,75 @@ async def task_worker():
async def do_enable(params: EnablePackParams) -> str:
cnr_id = params.cnr_id
logging.debug("[ComfyUI-Manager] Enabling node: cnr_id=%s", cnr_id)
core.unified_manager.unified_enable(cnr_id)
# Parse node spec if it contains version/hash (e.g., "NodeName@hash")
node_name = cnr_id
version_spec = None
git_hash = None
if '@' in cnr_id:
node_spec = core.unified_manager.resolve_node_spec(cnr_id)
if node_spec is not None:
parsed_node_name, parsed_version_spec, is_specified = node_spec
logging.debug(
"[ComfyUI-Manager] Parsed node spec: name=%s, version=%s",
parsed_node_name,
parsed_version_spec
)
node_name = parsed_node_name
version_spec = parsed_version_spec
# If version_spec looks like a git hash (40 hex chars), save it for checkout
if parsed_version_spec and len(parsed_version_spec) == 40 and all(c in '0123456789abcdef' for c in parsed_version_spec.lower()):
git_hash = parsed_version_spec
logging.debug("[ComfyUI-Manager] Detected git hash for checkout: %s", git_hash)
else:
# If parsing fails, try splitting manually
parts = cnr_id.split('@')
node_name = parts[0]
if len(parts) > 1:
version_spec = parts[1]
if len(parts[1]) == 40:
git_hash = parts[1]
logging.debug(
"[ComfyUI-Manager] Manual split result: name=%s, version=%s, hash=%s",
node_name,
version_spec,
git_hash
)
# Normalize node_name for case-insensitive matching
node_name = cnr_utils.normalize_package_name(node_name)
# Enable the nodepack with version_spec
res = core.unified_manager.unified_enable(node_name, version_spec)
if not res or not res.result:
return f"Failed to enable: '{cnr_id}'"
# If git hash is specified and enable succeeded, checkout the specific commit
if git_hash and res.target_path:
try:
from . import manager_core
checkout_success = manager_core.checkout_git_commit(res.target_path, git_hash)
if checkout_success:
logging.info(
"[ComfyUI-Manager] Successfully checked out commit %s for %s",
git_hash[:8],
node_name
)
else:
logging.warning(
"[ComfyUI-Manager] Enable succeeded but failed to checkout commit %s for %s",
git_hash[:8],
node_name
)
except Exception as e:
logging.error(
"[ComfyUI-Manager] Enable succeeded but error during git checkout: %s",
e
)
traceback.print_exc()
return OperationResult.success.value
async def do_update(params: UpdatePackParams) -> dict[str, str]:
@@ -905,15 +958,38 @@ async def task_worker():
try:
res = core.unified_manager.unified_update(node_name, node_ver)
if res.ver == "unknown":
url = core.unified_manager.unknown_active_nodes[node_name][0]
# Get active package using modern unified manager
active_pack = core.unified_manager.get_active_pack(node_name)
if active_pack is None:
# Fallback if package not found
url = None
title = node_name
elif res.ver == "unknown":
# For unknown packages, use repo_url if available
url = active_pack.repo_url
try:
title = os.path.basename(url)
title = os.path.basename(url) if url else node_name
except Exception:
title = node_name
else:
url = core.unified_manager.cnr_map[node_name].get("repository")
title = core.unified_manager.cnr_map[node_name]["name"]
# For CNR packages, get info from CNR registry
try:
from ..common import cnr_utils
compact_url = core.git_utils.compact_url(active_pack.repo_url) if active_pack.repo_url else None
cnr_info = cnr_utils.get_nodepack_by_url(compact_url) if compact_url else None
if cnr_info:
url = cnr_info.get("repository")
title = cnr_info.get("name", node_name)
else:
# Fallback for CNR packages without registry info
url = active_pack.repo_url
title = node_name
except Exception:
# Fallback if CNR lookup fails
url = active_pack.repo_url
title = node_name
manager_util.clear_pip_cache()
@@ -1012,17 +1088,13 @@ async def task_worker():
logging.error(SECURITY_MESSAGE_MIDDLE)
return OperationResult.failed.value
node_name = params.node_name
is_unknown = params.is_unknown
# Normalize node_name for case-insensitive matching
node_name = cnr_utils.normalize_package_name(params.node_name)
logging.debug(
"[ComfyUI-Manager] Uninstalling node: name=%s, is_unknown=%s",
node_name,
is_unknown,
)
logging.debug("[ComfyUI-Manager] Uninstalling node: name=%s", node_name)
try:
res = core.unified_manager.unified_uninstall(node_name, is_unknown)
res = core.unified_manager.unified_uninstall(node_name)
if res.result:
return OperationResult.success.value
@@ -1038,14 +1110,33 @@ async def task_worker():
async def do_disable(params: DisablePackParams) -> str:
node_name = params.node_name
logging.debug(
"[ComfyUI-Manager] Disabling node: name=%s, is_unknown=%s",
node_name,
params.is_unknown,
)
logging.debug("[ComfyUI-Manager] Disabling node: name=%s", node_name)
try:
res = core.unified_manager.unified_disable(node_name, params.is_unknown)
# Parse node spec if it contains version/hash (e.g., "NodeName@hash")
# Extract just the node name for disable operation
if '@' in node_name:
node_spec = core.unified_manager.resolve_node_spec(node_name)
if node_spec is not None:
parsed_node_name, version_spec, is_specified = node_spec
logging.debug(
"[ComfyUI-Manager] Parsed node spec: name=%s, version=%s",
parsed_node_name,
version_spec
)
node_name = parsed_node_name
else:
# If parsing fails, try splitting manually
node_name = node_name.split('@')[0]
logging.debug(
"[ComfyUI-Manager] Manual split result: name=%s",
node_name
)
# Normalize node_name for case-insensitive matching
node_name = cnr_utils.normalize_package_name(node_name)
res = core.unified_manager.unified_disable(node_name)
if res:
return OperationResult.success.value
@@ -1155,6 +1246,9 @@ async def task_worker():
item, task_index = task
kind = item.kind
# Reload installed packages before each task to ensure we have the latest state
core.unified_manager.reload()
logging.debug(
"[ComfyUI-Manager] Processing task: kind=%s, ui_id=%s, client_id=%s, task_index=%d",
kind,
@@ -1357,7 +1451,16 @@ async def get_history(request):
}
history = filtered_history
return web.json_response({"history": history}, content_type="application/json")
# Convert TaskHistoryItem models to JSON-serializable dicts
if isinstance(history, dict):
history_json = {
task_id: task_data.model_dump(mode="json") if hasattr(task_data, "model_dump") else task_data
for task_id, task_data in history.items()
}
else:
history_json = history.model_dump(mode="json") if hasattr(history, "model_dump") else history
return web.json_response({"history": history_json}, content_type="application/json")
except Exception as e:
logging.error(f"[ComfyUI-Manager] /v2/manager/queue/history - {e}")
@@ -1365,42 +1468,6 @@ async def get_history(request):
return web.Response(status=400)
@routes.get("/v2/customnode/getmappings")
async def fetch_customnode_mappings(request):
"""
provide unified (node -> node pack) mapping list
"""
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")
json_obj = core.map_to_unified_keys(json_obj)
if nickname_mode:
json_obj = node_pack_utils.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")
@routes.get("/v2/customnode/fetch_updates")
async def fetch_updates(request):
"""
@@ -1448,44 +1515,22 @@ async def _update_all(params: UpdateAllQueryParams) -> web.Response:
mode,
)
if mode == ManagerDatabaseSource.local.value:
channel = "local"
else:
channel = core.get_config()["channel_url"]
await core.unified_manager.reload(mode)
await core.unified_manager.get_custom_nodes(channel, mode)
update_count = 0
for k, v in core.unified_manager.active_nodes.items():
if k == "comfyui-manager":
# skip updating comfyui-manager if desktop version
if os.environ.get("__COMFYUI_DESKTOP_VERSION__"):
continue
update_task = QueueTaskItem(
kind=OperationType.update.value,
ui_id=f"{base_ui_id}_{k}", # Use client's base ui_id + node name
client_id=client_id,
params=UpdatePackParams(node_name=k, node_ver=v[0]),
)
task_queue.put(update_task)
update_count += 1
for k, v in core.unified_manager.unknown_active_nodes.items():
if k == "comfyui-manager":
# skip updating comfyui-manager if desktop version
if os.environ.get("__COMFYUI_DESKTOP_VERSION__"):
continue
update_task = QueueTaskItem(
kind=OperationType.update.value,
ui_id=f"{base_ui_id}_{k}", # Use client's base ui_id + node name
client_id=client_id,
params=UpdatePackParams(node_name=k, node_ver="unknown"),
)
task_queue.put(update_task)
update_count += 1
# Iterate through all installed packages using modern unified manager
for packname, package_list in core.unified_manager.installed_node_packages.items():
# Find enabled packages for this packname
for package in package_list:
if package.is_enabled:
update_task = QueueTaskItem(
kind=OperationType.update.value,
ui_id=f"{base_ui_id}_{packname}", # Use client's base ui_id + node name
client_id=client_id,
params=UpdatePackParams(node_name=packname, node_ver=package.version),
)
task_queue.put(update_task)
update_count += 1
# Only create one update task per packname (first enabled package)
break
logging.debug(
"[ComfyUI-Manager] Update all queued %d tasks for client_id=%s",
@@ -1505,7 +1550,7 @@ async def is_legacy_manager_ui(request):
# freeze imported version
startup_time_installed_node_packs = core.get_installed_node_packs()
startup_time_installed_node_packs = core.get_installed_nodepacks()
@routes.get("/v2/customnode/installed")
@@ -1515,7 +1560,7 @@ async def installed_list(request):
if mode == "imported":
res = startup_time_installed_node_packs
else:
res = core.get_installed_node_packs()
res = core.get_installed_nodepacks()
return web.json_response(res, content_type="application/json")
@@ -1661,58 +1706,53 @@ async def import_fail_info(request):
async def import_fail_info_bulk(request):
try:
json_data = await request.json()
# Validate input using Pydantic model
request_data = ImportFailInfoBulkRequest.model_validate(json_data)
# Ensure we have either cnr_ids or urls
if not request_data.cnr_ids and not request_data.urls:
# Basic validation - ensure we have either cnr_ids or urls
if not isinstance(json_data, dict):
return web.Response(status=400, text="Request body must be a JSON object")
if "cnr_ids" not in json_data and "urls" not in json_data:
return web.Response(
status=400, text="Either 'cnr_ids' or 'urls' field is required"
)
await core.unified_manager.reload('cache')
await core.unified_manager.get_custom_nodes('default', 'cache')
results = {}
if request_data.cnr_ids:
for cnr_id in request_data.cnr_ids:
if "cnr_ids" in json_data:
if not isinstance(json_data["cnr_ids"], list):
return web.Response(status=400, text="'cnr_ids' must be an array")
for cnr_id in json_data["cnr_ids"]:
if not isinstance(cnr_id, str):
results[cnr_id] = {"error": "cnr_id must be a string"}
continue
module_name = core.unified_manager.get_module_name(cnr_id)
if module_name is not None:
info = cm_global.error_dict.get(module_name)
if info is not None:
# Convert error_dict format to API spec format
results[cnr_id] = {
'error': info.get('msg', ''),
'traceback': info.get('traceback', '')
}
results[cnr_id] = info
else:
results[cnr_id] = None
else:
results[cnr_id] = None
if request_data.urls:
for url in request_data.urls:
if "urls" in json_data:
if not isinstance(json_data["urls"], list):
return web.Response(status=400, text="'urls' must be an array")
for url in json_data["urls"]:
if not isinstance(url, str):
results[url] = {"error": "url must be a string"}
continue
module_name = core.unified_manager.get_module_name(url)
if module_name is not None:
info = cm_global.error_dict.get(module_name)
if info is not None:
# Convert error_dict format to API spec format
results[url] = {
'error': info.get('msg', ''),
'traceback': info.get('traceback', '')
}
results[url] = info
else:
results[url] = None
else:
results[url] = None
# Return results directly as JSON
return web.json_response(results, content_type="application/json")
except ValidationError as e:
logging.error(f"[ComfyUI-Manager] Invalid request data: {e}")
return web.Response(status=400, text=f"Invalid request data: {e}")
return web.json_response(results)
except Exception as e:
logging.error(f"[ComfyUI-Manager] Error processing bulk import fail info: {e}")
return web.Response(status=500, text="Internal server error")
@@ -1994,88 +2034,6 @@ async def get_version(request):
return web.Response(text=core.version_str, status=200)
async def _confirm_try_install(sender, custom_node_url, msg):
json_obj = await core.get_data_by_mode("default", "custom-node-list.json")
sender = manager_util.sanitize_tag(sender)
msg = manager_util.sanitize_tag(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:
logging.error(
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)
async def default_cache_update():
core.refresh_channel_dict()
channel_url = core.get_config()["channel_url"]
async def get_cache(filename):
try:
if core.get_config()["default_cache_as_channel_url"]:
uri = f"{channel_url}/{filename}"
else:
uri = f"{core.DEFAULT_CHANNEL}/{filename}"
cache_uri = str(manager_util.simple_hash(uri)) + "_" + filename
cache_uri = os.path.join(manager_util.cache_dir, cache_uri)
json_obj = await manager_util.get_data(uri, True)
with manager_util.cache_lock:
with open(cache_uri, "w", encoding="utf-8") as file:
json.dump(json_obj, file, indent=4, sort_keys=True)
logging.debug(f"[ComfyUI-Manager] default cache updated: {uri}")
except Exception as e:
logging.error(
f"[ComfyUI-Manager] Failed to perform initial fetching '{filename}': {e}"
)
traceback.print_exc()
if core.get_config()["network_mode"] != "offline":
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")
e = get_cache("github-stats.json")
await asyncio.gather(a, b, c, d, e)
if core.get_config()["network_mode"] == "private":
logging.info(
"[ComfyUI-Manager] The private comfyregistry is not yet supported in `network_mode=private`."
)
else:
# load at least once
await core.unified_manager.reload(
ManagerDatabaseSource.remote.value, dont_wait=False
)
await core.unified_manager.get_custom_nodes(
channel_url, ManagerDatabaseSource.remote.value
)
else:
await core.unified_manager.reload(
ManagerDatabaseSource.remote.value, dont_wait=False, update_cnr_map=False
)
logging.info("[ComfyUI-Manager] All startup tasks have been completed.")
threading.Thread(target=lambda: asyncio.run(default_cache_update())).start()
if not os.path.exists(context.manager_config_path):
core.get_config()
core.write_config()

View File

@@ -17,25 +17,6 @@ def get_model_dir(data, show_log=False):
if any(char in data["filename"] for char in {"/", "\\", ":"}):
return None
def resolve_custom_node(save_path):
save_path = save_path[13:] # remove 'custom_nodes/'
# NOTE: Validate to prevent path traversal.
if save_path.startswith(os.path.sep) or ":" in save_path:
return None
repo_name = save_path.replace("\\", "/").split("/")[
0
] # get custom node 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)
else:
return None
if data["save_path"] != "default":
if ".." in data["save_path"] or data["save_path"].startswith("/"):
if show_log:
@@ -45,13 +26,8 @@ def get_model_dir(data, show_log=False):
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:
if show_log:
logging.info(
f"[ComfyUI-Manager] The target custom node for model download is not installed: {data['save_path']}"
)
return None
logging.warning("The feature to download models into the custom node path is no longer supported.")
return None
else:
base_model = os.path.join(models_base, data["save_path"])
else:

View File

@@ -1,65 +0,0 @@
import concurrent.futures
from comfyui_manager.glob import manager_core as core
def check_state_of_git_node_pack(
node_packs, 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_state_of_git_node_pack_single(
item, do_fetch, do_update_check, do_update
)
with concurrent.futures.ThreadPoolExecutor(4) as executor:
for k, v in node_packs.items():
if v.get("active_version") in ["unknown", "nightly"]:
executor.submit(process_custom_node, v)
if do_fetch:
print("\x1b[2K\rFetching done.")
elif do_update:
update_exists = any(
item.get("updatable", False) for item in node_packs.values()
)
if update_exists:
print("\x1b[2K\rUpdate done.")
else:
print("\x1b[2K\rAll extensions are already up-to-date.")
elif do_update_check:
print("\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

View File

@@ -1,6 +1,6 @@
from comfyui_manager.glob import manager_core as core
from comfy.cli_args import args
from comfyui_manager.data_models import SecurityLevel, RiskLevel, ManagerDatabaseSource
from comfyui_manager.data_models import SecurityLevel, RiskLevel
def is_loopback(address):
@@ -38,30 +38,3 @@ def is_allowed_security_level(level):
return core.get_config()['security_level'] in [SecurityLevel.weak.value, SecurityLevel.normal.value, SecurityLevel.normal_.value]
else:
return True
async def get_risky_level(files, pip_packages):
json_data1 = await core.get_data_by_mode(ManagerDatabaseSource.local.value, "custom-node-list.json")
json_data2 = await core.get_data_by_mode(
ManagerDatabaseSource.cache.value,
"custom-node-list.json",
channel_url="https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main",
)
all_urls = set()
for x in json_data1["custom_nodes"] + json_data2["custom_nodes"]:
all_urls.update(x.get("files", []))
for x in files:
if x not in all_urls:
return RiskLevel.high_.value
all_pip_packages = set()
for x in json_data1["custom_nodes"] + json_data2["custom_nodes"]:
all_pip_packages.update(x.get("pip", []))
for p in pip_packages:
if p not in all_pip_packages:
return RiskLevel.block.value
return RiskLevel.middle_.value

View File

@@ -41,12 +41,11 @@ from ..common.enums import NetworkMode, SecurityLevel, DBMode
from ..common import context
version_code = [4, 0, 3]
version_code = [5, 0]
version_str = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
DEFAULT_CHANNEL = "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main"
DEFAULT_CHANNEL_LEGACY = "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main"
default_custom_nodes_path = None
@@ -161,7 +160,7 @@ comfy_ui_revision = "Unknown"
comfy_ui_commit_datetime = datetime(1900, 1, 1, 0, 0, 0)
channel_dict = None
valid_channels = {'default', 'local', DEFAULT_CHANNEL, DEFAULT_CHANNEL_LEGACY}
valid_channels = {'default', 'local'}
channel_list = None
@@ -1391,7 +1390,6 @@ class UnifiedManager:
return ManagedResult('skip')
elif self.is_disabled(node_id):
return self.unified_enable(node_id)
else:
version_spec = self.resolve_unspecified_version(node_id)

View File

@@ -1072,15 +1072,12 @@ async def fetch_customnode_list(request):
if channel != 'local':
found = 'custom'
if channel == core.DEFAULT_CHANNEL or channel == core.DEFAULT_CHANNEL_LEGACY:
channel = 'default'
else:
for name, url in core.get_channel_dict().items():
if url == channel:
found = name
break
for name, url in core.get_channel_dict().items():
if url == channel:
found = name
break
channel = found
channel = found
result = dict(channel=channel, node_packs=node_packs.to_dict())

View File

@@ -10,16 +10,6 @@ import hashlib
import folder_paths
from server import PromptServer
import logging
import sys
try:
from nio import AsyncClient, LoginResponse, UploadResponse
matrix_nio_is_available = True
except Exception:
logging.warning(f"[ComfyUI-Manager] The matrix sharing feature has been disabled because the `matrix-nio` dependency is not installed.\n\tTo use this feature, please run the following command:\n\t{sys.executable} -m pip install matrix-nio\n")
matrix_nio_is_available = False
def extract_model_file_names(json_data):
@@ -202,14 +192,6 @@ async def get_esheep_workflow_and_images(request):
return web.Response(status=200, text=json.dumps(data))
@PromptServer.instance.routes.get("/v2/manager/get_matrix_dep_status")
async def get_matrix_dep_status(request):
if matrix_nio_is_available:
return web.Response(status=200, text='available')
else:
return web.Response(status=200, text='unavailable')
def set_matrix_auth(json_data):
homeserver = json_data['homeserver']
username = json_data['username']
@@ -349,12 +331,14 @@ async def share_art(request):
workflowId = upload_workflow_json["workflowId"]
# check if the user has provided Matrix credentials
if matrix_nio_is_available and "matrix" in share_destinations:
if "matrix" in share_destinations:
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
filename = os.path.basename(asset_filepath)
content_type = assetFileType
try:
from nio import AsyncClient, LoginResponse, UploadResponse
homeserver = 'matrix.org'
if matrix_auth:
homeserver = matrix_auth.get('homeserver', 'matrix.org')