refactor: centralize filename extraction, remote URL normalization, and storage abstractions

This commit is contained in:
Dariusz L
2026-07-16 10:13:38 +02:00
parent 32394135e9
commit 8823049f9e
19 changed files with 222 additions and 144 deletions
+2 -2
View File
@@ -855,7 +855,7 @@ def _resolve_aria2c_executable(settings: Optional[Dict[str, Any]] = None) -> str
configured = str(active_settings.get("aria2c_path") or "").strip()
candidate = os.path.expandvars(os.path.expanduser(configured or "aria2c"))
expected_names = {"aria2c", "aria2c.exe"}
candidate_name = os.path.basename(candidate).lower()
candidate_name = get_filename_from_path(candidate).lower()
has_path_component = bool(
os.path.isabs(candidate)
or os.path.dirname(candidate)
@@ -877,7 +877,7 @@ def _resolve_aria2c_executable(settings: Optional[Dict[str, Any]] = None) -> str
if candidate_name in expected_names:
resolved = shutil.which(candidate)
if resolved and os.path.basename(resolved).lower() in expected_names:
if resolved and get_filename_from_path(resolved).lower() in expected_names:
return os.path.realpath(os.path.abspath(resolved))
raise Aria2Error(
+6 -4
View File
@@ -13,6 +13,8 @@ from difflib import SequenceMatcher
from .log_system import create_module_logger
log = create_module_logger(__name__)
from .path_utils import get_filename_from_path
try:
from rapidfuzz import fuzz as rapidfuzz_fuzz
except ImportError:
@@ -121,7 +123,7 @@ def find_matches(
# Extract just the filename from target_model (remove any subfolder paths)
# target_model might be just a filename or might include subfolder paths
target_filename = os.path.basename(target_model_normalized)
target_filename = get_filename_from_path(target_model_normalized)
# Normalize target filename once for exact match comparisons
target_norm = normalize_filename(target_filename)
@@ -135,7 +137,7 @@ def find_matches(
# If no filename key, try to extract from path or relative_path
if not candidate_filename:
if candidate_path:
candidate_filename = os.path.basename(candidate_path)
candidate_filename = get_filename_from_path(candidate_path)
if not candidate_filename:
continue
@@ -402,7 +404,7 @@ def clean_filename_for_search(filename: str) -> str:
"""
if not filename:
return ""
basename = os.path.basename(filename).strip()
basename = get_filename_from_path(filename).strip()
base = os.path.splitext(basename)[0]
pattern = r"[-_]?(" + "|".join(PRECISION_FORMAT_SUFFIXES) + r").*$"
return re.sub(pattern, "", base, flags=re.IGNORECASE).strip(" -_")
@@ -413,7 +415,7 @@ def build_filename_search_queries(filename: str) -> List[str]:
Build a list of candidate search queries from a filename by stripping
extensions and common precision/format suffixes (e.g. fp16, bf16, pruned).
"""
basename = os.path.basename(filename or "").strip()
basename = get_filename_from_path(filename).strip()
if not basename:
return []
+3 -3
View File
@@ -12,7 +12,7 @@ from .log_system import create_module_logger
log = create_module_logger(__name__)
from .path_utils import get_path_identity
from .path_utils import get_path_identity, get_filename_from_path
# Import folder_paths lazily - it may not be available until ComfyUI is initialized
try:
import folder_paths
@@ -139,11 +139,11 @@ def scan_directory(
try:
relative_path = os.path.relpath(root, base_directory)
except ValueError:
relative_path = os.path.basename(root)
relative_path = get_filename_from_path(root)
models.append(
{
"filename": os.path.basename(root),
"filename": get_filename_from_path(root),
"path": root,
"relative_path": relative_path,
"category": category,
+5 -6
View File
@@ -45,6 +45,7 @@ from ..type_utils import (
normalize_model_file_info,
build_search_result,
normalize_sha256,
prepare_remote_size_probe_url,
)
from ..progress import report_progress, get_progress_reporter
from ..log_system import create_module_logger
@@ -541,16 +542,14 @@ def _prepare_size_probe_url(url: Any) -> Optional[str]:
if not normalized:
return None
normalized = prepare_remote_size_probe_url(normalized)
if not normalized:
return None
parsed = urlparse(normalized)
host = parsed.hostname
path = parsed.path or ""
if host_matches_domain(host, "huggingface.co") and "/blob/" in path:
normalized = normalized.replace("/blob/", "/resolve/", 1)
parsed = urlparse(normalized)
host = parsed.hostname
path = parsed.path or ""
if host_matches_domain(host, "civarchive.com") and not path.startswith("/api/download/"):
return None
if host_matches_domain(host, "civitai.com", "civitai.red") and not path.startswith("/api/download/"):
+10 -18
View File
@@ -17,9 +17,9 @@ from ..progress import report_progress, get_progress_reporter
from ..log_system import create_module_logger
log = create_module_logger(__name__)
from ..path_utils import write_json_atomic, METADATA_DIR, read_json_safe
from ..path_utils import write_json_atomic, METADATA_DIR, read_json_safe, get_filename_from_path
from ..network_utils import host_matches_domain, request_source_response, request_source_json
from ..type_utils import check_credential_http, parse_size_header as _parse_size_header, fetch_remote_file_size, fetch_remote_file_size_cached, clear_remote_size_cache, extract_file_size
from ..type_utils import check_credential_http, parse_size_header as _parse_size_header, fetch_remote_file_size, fetch_remote_file_size_cached, clear_remote_size_cache, extract_file_size, prepare_remote_size_probe_url
from ..matcher import build_filename_search_queries, clean_filename_for_search
HF_API_URL = "https://huggingface.co/api"
@@ -171,7 +171,7 @@ def _build_author_index_from_models(
{
"repo_id": repo_id,
"path": file_path,
"filename": os.path.basename(file_path),
"filename": get_filename_from_path(file_path),
"size": sibling.get("size"),
}
)
@@ -330,15 +330,7 @@ def get_huggingface_download_url(repo: str, filename: str, branch: str = "main")
def _normalize_huggingface_size_probe_url(url: str) -> Optional[str]:
if not isinstance(url, str) or not url.strip():
return None
value = url.strip()
parsed = urlparse(value)
if not host_matches_domain(parsed.hostname, "huggingface.co"):
return None
if "/blob/" in parsed.path:
value = value.replace("/blob/", "/resolve/", 1)
return value
return prepare_remote_size_probe_url(url)
@@ -375,7 +367,7 @@ def _build_huggingface_result(
return {
"source": "huggingface",
"repo_id": repo_id,
"filename": os.path.basename(file_path),
"filename": get_filename_from_path(file_path),
"path": file_path,
"url": download_url,
"size": size,
@@ -424,7 +416,7 @@ def _find_matching_file_in_repo(
if not file_path:
continue
file_name = os.path.basename(file_path)
file_name = get_filename_from_path(file_path)
file_name_lower = file_name.lower()
file_base = os.path.splitext(file_name_lower)[0]
@@ -445,7 +437,7 @@ def _find_matching_file_in_repo(
if not file_path:
continue
file_name = os.path.basename(file_path)
file_name = get_filename_from_path(file_path)
file_name_lower = file_name.lower()
file_base = os.path.splitext(file_name_lower)[0]
@@ -481,7 +473,7 @@ def _find_matching_file_in_author_index(
if not repo_id or not file_path:
continue
file_name = os.path.basename(file_path)
file_name = get_filename_from_path(file_path)
file_name_lower = file_name.lower()
if file_name_lower == filename_lower or file_path.lower().endswith(
filename_lower
@@ -503,7 +495,7 @@ def _find_matching_file_in_author_index(
if not repo_id or not file_path:
continue
file_name = os.path.basename(file_path)
file_name = get_filename_from_path(file_path)
file_name_lower = file_name.lower()
file_base = os.path.splitext(file_name_lower)[0]
file_path_lower = file_path.lower()
@@ -1021,7 +1013,7 @@ def get_repo_files(repo: str, token: Optional[str] = None) -> List[Dict[str, Any
size = _fetch_remote_file_size_bytes(url, headers=headers)
files.append(
{
"filename": os.path.basename(path),
"filename": get_filename_from_path(path),
"path": path,
"url": url,
"size": size,
+3 -2
View File
@@ -19,6 +19,7 @@ from ..matcher import (
calculate_candidate_rank,
should_update_best_match,
)
from ..path_utils import get_filename_from_path
from ..type_utils import (
select_primary_model_file,
get_generic_filename_tokens,
@@ -113,7 +114,7 @@ def _normalize_model_type(model_type: Optional[str]) -> str:
def _extract_search_tokens(query: str) -> Dict[str, List[str]]:
"""Split filename/model query into useful content/version tokens."""
base_query = os.path.splitext(os.path.basename(query or ""))[0]
base_query = os.path.splitext(get_filename_from_path(query))[0]
raw_tokens = [
token.lower()
for token in re.split(r"[^a-zA-Z0-9]+", base_query)
@@ -662,7 +663,7 @@ def search_lora_manager_archive_for_file(
"""
Search the archive for the best model/version match for a filename.
"""
search_query = os.path.splitext(os.path.basename(filename or ""))[0]
search_query = os.path.splitext(get_filename_from_path(filename))[0]
if not search_query:
return None
+31 -2
View File
@@ -605,6 +605,7 @@ def looks_like_model_file(url: str, expected_filename: str = "") -> bool:
import os
from urllib.parse import unquote, urlparse
from .network_utils import host_matches_domain
from .path_utils import get_filename_from_path
text = str(url or "").strip()
if not text.startswith(("http://", "https://", "hf://")):
@@ -628,8 +629,8 @@ def looks_like_model_file(url: str, expected_filename: str = "") -> bool:
) and "/api/download/" in path:
return True
basename = os.path.basename(path).lower()
expected = os.path.basename(str(expected_filename or "")).lower()
basename = get_filename_from_path(path).lower()
expected = get_filename_from_path(str(expected_filename or "")).lower()
if expected and basename == expected:
return True
@@ -829,6 +830,34 @@ def extract_trained_words(*values: Any) -> List[str]:
_remote_size_cache: Dict[tuple[str, Optional[str]], Optional[int]] = {}
def prepare_remote_size_probe_url(url: Any) -> Optional[str]:
"""
Normalize a remote URL to be suitable for file size probing.
Converts HuggingFace '/blob/' URLs to '/resolve/' URLs.
"""
from urllib.parse import urlparse
from .network_utils import host_matches_domain
if not isinstance(url, str) or not url.strip():
return None
value = url.strip()
if value.startswith("//"):
value = f"https:{value}"
try:
parsed = urlparse(value)
host = parsed.hostname
path = parsed.path or ""
except Exception:
return None
if host and host_matches_domain(host, "huggingface.co") and "/blob/" in path:
value = value.replace("/blob/", "/resolve/", 1)
return value
def fetch_remote_file_size_cached(
url: str,
headers: Optional[Dict[str, str]] = None,
+2 -2
View File
@@ -9,7 +9,7 @@ from typing import Dict, Any, List, Optional
from .log_system import create_module_logger
log = create_module_logger(__name__)
from .path_utils import is_path_within
from .path_utils import is_path_within, get_filename_from_path
@@ -78,7 +78,7 @@ def convert_to_relative_path(
pass
# Fallback: return just the filename
return os.path.basename(absolute_path)
return get_filename_from_path(absolute_path)
def get_base_directory_for_model(
+8
View File
@@ -212,6 +212,14 @@ class TestRefactoringUnification(unittest.TestCase):
self.assertEqual(state["status"], "running")
self.assertEqual(state["percent"], 50)
def test_get_filename_from_path(self):
from core.path_utils import get_filename_from_path
self.assertEqual(get_filename_from_path("some/path/to/model.safetensors"), "model.safetensors")
self.assertEqual(get_filename_from_path("some\\path\\to\\model.safetensors"), "model.safetensors")
self.assertEqual(get_filename_from_path("model.safetensors"), "model.safetensors")
self.assertEqual(get_filename_from_path(""), "")
self.assertEqual(get_filename_from_path(None), "")
if __name__ == "__main__":
unittest.main()
@@ -5,6 +5,7 @@ import { createModuleLogger } from "../../log_system/log_funcs.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { getModelCardUrl } from "../utils/url_utils.js";
import { getCivitaiModelUrl } from "../globals.js";
import { normalizePathIdentity } from "../utils/html_utils.js";
const log = createModuleLogger('resolve_download_methods');
@@ -595,11 +596,7 @@ export const resolveDownloadMethods = {
},
normalizeLocalMatchPathIdentity(value = '') {
return String(value || '')
.trim()
.replace(/\\/g, '/')
.replace(/\/+/g, '/')
.toLowerCase();
return normalizePathIdentity(value);
},
getLocalMatchAbsolutePathIdentity(match = {}) {
+7 -18
View File
@@ -1,5 +1,6 @@
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { api } from "../../../../scripts/api.js";
import { safeStorage } from "./utils/html_utils.js";
import { dialogShellMethods } from "./shell/dialog_shell_methods.js";
import { lifecycleGraphMethods } from "./shell/lifecycle_graph_methods.js";
import { workflowStateMethods } from "./shell/workflow_state_methods.js";
@@ -73,24 +74,12 @@ export class ResolverManagerDialog extends ComfyDialog {
this.showResolvedModelsStorageKey = "model_resolver_show_resolved_models";
this.showAutoDownloadModelsStorageKey = "model_resolver_show_auto_download_models";
this.localMatchAlternativesCollapsedStorageKey = "model_resolver_local_match_alternatives_collapsed";
try {
this.showResolvedModels = localStorage.getItem(this.showResolvedModelsStorageKey) === '1';
} catch (e) {
this.showResolvedModels = false;
}
try {
const storedShowAutoDownload = localStorage.getItem(this.showAutoDownloadModelsStorageKey);
this.showAutoDownloadModels = storedShowAutoDownload === null
? true
: storedShowAutoDownload === '1';
} catch (e) {
this.showAutoDownloadModels = true;
}
try {
this.localMatchAlternativesCollapsed = localStorage.getItem(this.localMatchAlternativesCollapsedStorageKey) === '1';
} catch (e) {
this.localMatchAlternativesCollapsed = false;
}
this.showResolvedModels = safeStorage.getItem(this.showResolvedModelsStorageKey) === '1';
const storedShowAutoDownload = safeStorage.getItem(this.showAutoDownloadModelsStorageKey);
this.showAutoDownloadModels = storedShowAutoDownload === null
? true
: storedShowAutoDownload === '1';
this.localMatchAlternativesCollapsed = safeStorage.getItem(this.localMatchAlternativesCollapsedStorageKey) === '1';
this.dockButton = null;
this.undockButton = null;
this._floatingRectBeforeDock = null;
@@ -3,6 +3,8 @@ import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { createFloatingTreePicker } from "../utils/tree_picker.js";
import { safeStorage } from "../utils/html_utils.js";
const localStorage = safeStorage;
export const downloadTargetMethods = {
/**
* Ensure all models are loaded for the dropdown.
+3 -1
View File
@@ -4,6 +4,8 @@ import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { getModelCardUrl } from "../utils/url_utils.js";
import { getCivitaiModelUrl } from "../globals.js";
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
const localStorage = safeStorage;
export const searchPanelMethods = {
/**
* Build stable cache key for a missing model entry
@@ -2458,7 +2460,7 @@ export const searchPanelMethods = {
getActiveDownloadInfoForLocalMatch(match = {}) {
const model = match.model || {};
const normalizePath = (value = '') => this.normalizeLocalMatchPathIdentity?.(value) || String(value || '').trim().replace(/\\/g, '/').replace(/\/+/g, '/').toLowerCase();
const normalizePath = (value = '') => normalizePathIdentity(value);
const joinPath = (...parts) => normalizePath(parts.filter(Boolean).join('/'));
const activeStatuses = new Set(['starting', 'downloading', 'paused', 'cancelling']);
const matchAbsolutePaths = [
+30 -49
View File
@@ -1,7 +1,8 @@
import { app } from "../../../../../scripts/app.js";
import { app } from "../../../../../scripts/app.js";
import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { safeStorage } from "../utils/html_utils.js";
export const dialogShellMethods = {
createHeader() {
// Create tabs
@@ -141,22 +142,18 @@ export const dialogShellMethods = {
return;
}
try {
const wh = JSON.parse(localStorage.getItem('model_resolver_modal_size_before_fs') || 'null');
if (wh?.w && wh?.h) {
el.style.width = `${wh.w}px`;
el.style.height = `${wh.h}px`;
}
const wh = JSON.parse(safeStorage.getItem('model_resolver_modal_size_before_fs') || 'null');
if (wh?.w && wh?.h) {
el.style.width = `${wh.w}px`;
el.style.height = `${wh.h}px`;
}
const pos = JSON.parse(localStorage.getItem('model_resolver_modal_pos') || 'null');
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
el.style.top = `${pos.top}px`;
el.style.left = `${pos.left}px`;
el.style.transform = 'none';
return;
}
} catch (e) {
// Fall back to centered floating geometry below.
const pos = JSON.parse(safeStorage.getItem('model_resolver_modal_pos') || 'null');
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
el.style.top = `${pos.top}px`;
el.style.left = `${pos.left}px`;
el.style.transform = 'none';
return;
}
el.style.top = '50%';
@@ -165,17 +162,11 @@ export const dialogShellMethods = {
},
rememberSidebarOpenMode(mode) {
try {
localStorage.setItem(this.sidebarOpenModeStorageKey, mode === 'floating' ? 'floating' : 'docked');
} catch (e) {}
safeStorage.setItem(this.sidebarOpenModeStorageKey, mode === 'floating' ? 'floating' : 'docked');
},
shouldOpenFromSidebarFloating() {
try {
return localStorage.getItem(this.sidebarOpenModeStorageKey) === 'floating';
} catch (e) {
return false;
}
return safeStorage.getItem(this.sidebarOpenModeStorageKey) === 'floating';
},
isVisible() {
@@ -415,10 +406,8 @@ export const dialogShellMethods = {
const btn = document.getElementById('model-resolver-fullscreen-toggle');
if (enable) {
// Save current size
try {
const rect = el.getBoundingClientRect();
localStorage.setItem('model_resolver_modal_size_before_fs', JSON.stringify({ w: Math.round(rect.width), h: Math.round(rect.height) }));
} catch (e) {}
const rect = el.getBoundingClientRect();
safeStorage.setItem('model_resolver_modal_size_before_fs', JSON.stringify({ w: Math.round(rect.width), h: Math.round(rect.height) }));
el.style.top = '0';
el.style.left = '0';
el.style.transform = 'none';
@@ -433,7 +422,7 @@ export const dialogShellMethods = {
btn.setAttribute('aria-label', 'Exit full screen');
this.setTooltip(btn, 'Exit full screen');
}
try { localStorage.setItem('model_resolver_modal_fullscreen', '1'); } catch (e) {}
safeStorage.setItem('model_resolver_modal_fullscreen', '1');
} else {
this.returnToDockedAfterFullscreen = false;
// Restore centered sizing
@@ -443,7 +432,7 @@ export const dialogShellMethods = {
el.style.resize = 'both';
// Restore saved pre-FS size if available
let wh = null;
try { wh = JSON.parse(localStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); } catch (e) {}
try { wh = JSON.parse(safeStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); } catch (e) {}
if (wh && wh.w && wh.h) {
el.style.width = `${wh.w}px`;
el.style.height = `${wh.h}px`;
@@ -452,18 +441,12 @@ export const dialogShellMethods = {
el.style.height = '700px';
}
// Restore last known position if available, else center
try {
const pos = JSON.parse(localStorage.getItem('model_resolver_modal_pos') || 'null');
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
el.style.top = `${pos.top}px`;
el.style.left = `${pos.left}px`;
el.style.transform = 'none';
} else {
el.style.top = '50%';
el.style.left = '50%';
el.style.transform = 'translate(-50%, -50%)';
}
} catch (e) {
const pos = JSON.parse(safeStorage.getItem('model_resolver_modal_pos') || 'null');
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
el.style.top = `${pos.top}px`;
el.style.left = `${pos.left}px`;
el.style.transform = 'none';
} else {
el.style.top = '50%';
el.style.left = '50%';
el.style.transform = 'translate(-50%, -50%)';
@@ -474,7 +457,7 @@ export const dialogShellMethods = {
this.setTooltip(btn, 'Enter full screen');
}
this.ensureModalHandleInViewport({ persist: true });
try { localStorage.setItem('model_resolver_modal_fullscreen', '0'); } catch (e) {}
safeStorage.setItem('model_resolver_modal_fullscreen', '0');
if (shouldReturnToDocked) {
this.dockToSidebar();
@@ -518,12 +501,10 @@ export const dialogShellMethods = {
saveModalPosition() {
if (this.docked) return;
try {
const el = this.element;
if (!el) return;
const rect = el.getBoundingClientRect();
localStorage.setItem('model_resolver_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) }));
} catch (e) { /* ignore */ }
const el = this.element;
if (!el) return;
const rect = el.getBoundingClientRect();
safeStorage.setItem('model_resolver_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) }));
},
ensureModalHandleInViewport({ persist = false } = {}) {
@@ -2,6 +2,7 @@ import { app } from "../../../../../scripts/app.js";
import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { safeStorage } from "../utils/html_utils.js";
export const lifecycleGraphMethods = {
async show(workflow = null) {
this.undockToFloating({ persist: false });
@@ -36,11 +37,8 @@ export const lifecycleGraphMethods = {
await this.ensureAllModelsLoaded();
await this.ensureDownloadDirectoriesLoaded();
// Restore fullscreen state if enabled
try {
const fs = localStorage.getItem('model_resolver_modal_fullscreen');
if (!this.docked && restoreFullscreen && fs === '1') this.setFullScreen(true);
} catch (e) { }
const fs = safeStorage.getItem('model_resolver_modal_fullscreen');
if (!this.docked && restoreFullscreen && fs === '1') this.setFullScreen(true);
// Attach drag handle event listener (only once)
this.attachDragHandleIfNeeded();
+64
View File
@@ -159,3 +159,67 @@ export async function pollBackgroundTask({
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
}
const memoryStorage = new Map();
let isLocalStorageAvailable = null;
function checkLocalStorage() {
if (isLocalStorageAvailable !== null) return isLocalStorageAvailable;
try {
localStorage.setItem('__test_storage__', '1');
localStorage.removeItem('__test_storage__');
isLocalStorageAvailable = true;
} catch (e) {
isLocalStorageAvailable = false;
console.warn('LocalStorage is not available, falling back to memory storage.', e);
}
return isLocalStorageAvailable;
}
export const safeStorage = {
getItem(key, defaultValue = null) {
if (checkLocalStorage()) {
try {
const val = localStorage.getItem(key);
return val !== null ? val : defaultValue;
} catch (e) {
console.warn(`safeStorage.getItem failed for key ${key}:`, e);
}
}
return memoryStorage.has(key) ? memoryStorage.get(key) : defaultValue;
},
setItem(key, value) {
const valStr = String(value);
if (checkLocalStorage()) {
try {
localStorage.setItem(key, valStr);
return true;
} catch (e) {
console.warn(`safeStorage.setItem failed for key ${key}:`, e);
}
}
memoryStorage.set(key, valStr);
return true;
},
removeItem(key) {
if (checkLocalStorage()) {
try {
localStorage.removeItem(key);
return true;
} catch (e) {
console.warn(`safeStorage.removeItem failed for key ${key}:`, e);
}
}
memoryStorage.delete(key);
return true;
}
};
export function normalizePathIdentity(value = '') {
return String(value || '')
.trim()
.replace(/[\\/]+/g, '/')
.replace(/\/+$/g, '')
.toLowerCase();
}
@@ -6,6 +6,8 @@ import { createFloatingTreePicker } from "../utils/tree_picker.js";
import { showNotification as showNotificationUtils } from "../../utils/notification_utils.js";
import { startSplitterDrag } from "../utils/splitter_drag.js";
import { getCivitaiModelUrl } from "../globals.js";
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
const localStorage = safeStorage;
export const missingBrowserMethods = {
getMissingFilename(missing = {}) {
@@ -180,10 +182,7 @@ export const missingBrowserMethods = {
const seenPaths = new Set();
const details = [];
const normalizePathIdentity = (value = '') => String(value || '')
.replace(/[\\/]+/g, '/')
.replace(/\/+$/g, '')
.toLowerCase();
// Uses imported normalizePathIdentity from html_utils.js
folderKeys.forEach(folderKey => {
const normalizedCategory = this.normalizeDownloadCategory?.(folderKey) || folderKey;
+36 -22
View File
@@ -2,7 +2,8 @@ import { app } from "../../../../../scripts/app.js";
import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { sanitizeDescriptionHtml } from "../utils/html_utils.js";
import { sanitizeDescriptionHtml, pollBackgroundTask, safeStorage } from "../utils/html_utils.js";
const localStorage = safeStorage;
import { getModelCardUrl } from "../utils/url_utils.js";
import { getCivitaiModelUrl } from "../globals.js";
export const modelInfoMethods = {
@@ -691,30 +692,43 @@ export const modelInfoMethods = {
onStart(progressId);
for (;;) {
await new Promise(resolve => setTimeout(resolve, 250));
if (checkAbort()) {
await onAbort(progressId);
throw new Error('Hash calculation cancelled');
}
const progress = await this.fetchJson(
`/model_resolver/calculate-file-hash/progress/${encodeURIComponent(progressId)}`,
{ silent: true },
'Get model hash progress'
);
let finalResult = null;
onProgress(progress?.percent || 0, progress?.message || '');
await pollBackgroundTask({
endpoint: `/model_resolver/calculate-file-hash/progress/${encodeURIComponent(progressId)}`,
tokenCheck: () => {
if (checkAbort()) {
onAbort(progressId);
return false;
}
return true;
},
onProgress: (progress) => {
onProgress(progress?.percent || 0, progress?.message || '');
},
isTerminal: (progress) => {
return ['done', 'cancelled', 'error'].includes(progress?.status);
},
onTerminal: (progress) => {
if (progress.status === 'done') {
finalResult = progress;
} else if (progress.status === 'cancelled') {
throw new Error('Hash calculation cancelled');
} else if (progress.status === 'error') {
throw new Error(progress.error || progress.message || 'Hash calculation failed');
}
},
onError: (err) => {
throw err;
},
fetchJson: this.fetchJson.bind(this)
});
if (progress?.status === 'done') {
return progress;
}
if (progress?.status === 'cancelled') {
throw new Error('Hash calculation cancelled');
}
if (progress?.status === 'error') {
throw new Error(progress.error || progress.message || 'Hash calculation failed');
}
if (checkAbort()) {
throw new Error('Hash calculation cancelled');
}
return finalResult;
},
async calculateLocalHashCandidatesForCompare(model = {}) {
+2 -1
View File
@@ -4,7 +4,8 @@ import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { LOG_LEVEL as DEFAULT_FRONTEND_LOG_LEVEL } from "../../log_system/config.js";
import { logger as frontendLogger } from "../../log_system/logger.js";
import { pollBackgroundTask } from "../utils/html_utils.js";
import { pollBackgroundTask, safeStorage } from "../utils/html_utils.js";
const localStorage = safeStorage;
const SETTINGS_MAP = [
{ serverKey: 'civitai_key', localKey: 'ModelResolver.civitaiApiKey', type: 'string', default: '' },
{ serverKey: 'civitai_session_token', localKey: 'ModelResolver.civitaiSessionToken', type: 'string', default: '' },