Unify local path joining

This commit is contained in:
Dariusz L
2026-08-06 21:19:29 +02:00
parent 43d83c2574
commit 335f886a4e
4 changed files with 54 additions and 32 deletions
@@ -9,6 +9,7 @@ import {
safeStorage,
} from '../web/resolver/utils/html_utils.js';
import { getModelCardUrl } from '../web/resolver/utils/url_utils.js';
import { joinPathPreservingStyle } from '../web/resolver/utils/path_utils.js';
import {
classifyLocalMatches,
matchesLocalModelDownload,
@@ -4027,6 +4028,29 @@ test('portable subfolder paths use forward slashes and join to the host path sty
);
});
test('shared local path join preserves host separators and empty-base contracts', () => {
assert.equal(
joinPathPreservingStyle('/models/loras', 'Pony\\Styles'),
'/models/loras/Pony/Styles'
);
assert.equal(
joinPathPreservingStyle('C:\\models\\loras', 'Pony/Styles'),
'C:\\models\\loras\\Pony\\Styles'
);
assert.equal(
joinPathPreservingStyle('\\\\server\\share\\', 'Pony/Styles'),
'\\\\server\\share\\Pony\\Styles'
);
assert.equal(
joinPathPreservingStyle('', 'Pony\\Styles', { normalizeRelativeWhenBaseEmpty: true }),
'Pony/Styles'
);
assert.equal(
joinPathPreservingStyle('', 'Pony\\Styles'),
'Pony\\Styles'
);
});
test('download subfolder and path template normalization apply the same segment rules', () => {
const normalizeDownloadPathValue = eval(`(${extractMethod(downloadTargetMethodsSource, 'normalizeDownloadPathValue')})`);
const dialog = {
+4 -13
View File
@@ -4,6 +4,7 @@ import { createFloatingTreePicker } from "../utils/tree_picker.js";
import { normalizeDownloadCategoryValue } from "../utils/category_utils.js";
import { safeStorage } from "../utils/html_utils.js";
import { getSha256Field } from "../utils/hash_utils.js";
import { joinPathPreservingStyle } from "../utils/path_utils.js";
import { matchesSearchText } from "../utils/search_utils.js";
const localStorage = safeStorage;
const invalidWindowsPathCharacters = new RegExp(
@@ -953,19 +954,9 @@ export const downloadTargetMethods = {
},
joinLocalPath(basePath = '', relativePath = '') {
const rawBase = String(basePath || '');
const relative = String(relativePath || '').replace(/^[/\\]+/, '');
const usesBackslash = /^[A-Za-z]:\\/.test(rawBase)
|| /^\\\\/.test(rawBase)
|| (!rawBase.includes('/') && rawBase.includes('\\'));
const separator = usesBackslash ? '\\' : '/';
const base = rawBase.replace(usesBackslash ? /[/\\]+$/ : /\/+$/, '')
|| (usesBackslash ? (/^\\+$/.test(rawBase) ? '\\' : '') : (/^\/+$/.test(rawBase) ? '/' : ''));
if (!base) return this.normalizePathToForward(relative);
if (!relative) return base;
const normalizedRelative = relative.replace(/[/\\]+/g, separator);
const joiner = base.endsWith(separator) ? '' : separator;
return `${base}${joiner}${normalizedRelative}`;
return joinPathPreservingStyle(basePath, relativePath, {
normalizeRelativeWhenBaseEmpty: true,
});
},
getDownloadTargetFolderContext(category = '', subfolder = '', baseDirectory = '') {
+23
View File
@@ -0,0 +1,23 @@
export function joinPathPreservingStyle(
basePath = '',
relativePath = '',
{ normalizeRelativeWhenBaseEmpty = false } = {},
) {
const rawBase = String(basePath || '');
const relative = String(relativePath || '').replace(/^[/\\]+/, '');
const usesBackslash = /^[A-Za-z]:\\/.test(rawBase)
|| /^\\\\/.test(rawBase)
|| (!rawBase.includes('/') && rawBase.includes('\\'));
const separator = usesBackslash ? '\\' : '/';
const base = rawBase.replace(usesBackslash ? /[/\\]+$/ : /\/+$/, '')
|| (usesBackslash ? (/^\\+$/.test(rawBase) ? '\\' : '') : (/^\/+$/.test(rawBase) ? '/' : ''));
if (!base) {
return normalizeRelativeWhenBaseEmpty
? relative.trim().replace(/\\/g, '/')
: relative;
}
if (!relative) return base;
const normalizedRelative = relative.replace(/[/\\]+/g, separator);
const joiner = base.endsWith(separator) ? '' : separator;
return `${base}${joiner}${normalizedRelative}`;
}
+3 -19
View File
@@ -5,6 +5,7 @@ import { getCivitaiModelUrl } from "../globals.js";
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
import { matchesWorkflowModelReference } from "../node_context_menu.js";
import { classifyLocalMatches } from "../utils/local_match_utils.js";
import { joinPathPreservingStyle } from "../utils/path_utils.js";
const MISSING_ROW_FALLBACK_HEIGHT = 70;
const MISSING_VIRTUAL_INITIAL_ROWS = 24;
@@ -1874,23 +1875,6 @@ export const missingBrowserMethods = {
return Array.from(byIdentity.values());
};
const joinLocalModelPath = (basePath = '', relativePath = '') => {
if (typeof this.joinLocalPath === 'function') {
return this.joinLocalPath(basePath, relativePath);
}
const rawBase = String(basePath || '');
const relative = String(relativePath || '').replace(/^[/\\]+/, '');
const usesBackslash = /^[A-Za-z]:\\/.test(rawBase)
|| /^\\\\/.test(rawBase)
|| (!rawBase.includes('/') && rawBase.includes('\\'));
const separator = usesBackslash ? '\\' : '/';
const base = rawBase.replace(usesBackslash ? /[/\\]+$/ : /\/+$/, '')
|| (usesBackslash ? (/^\\+$/.test(rawBase) ? '\\' : '') : (/^\/+$/.test(rawBase) ? '/' : ''));
if (!base) return relative;
if (!relative) return base;
const joiner = base.endsWith(separator) ? '' : separator;
return `${base}${joiner}${relative.replace(/[/\\]+/g, separator)}`;
};
const buildLocalFolderContext = (folderPath = '', name = 'Folder', category = '') => {
const path = String(folderPath || '').trim();
if (!path) return null;
@@ -1911,7 +1895,7 @@ export const missingBrowserMethods = {
const path = entry.fullPath || model.path || model.resolved_path || '';
if (!path) return null;
const directory = entry.folderPath
? joinLocalModelPath(entry.baseDirectory, entry.folderPath)
? joinPathPreservingStyle(entry.baseDirectory, entry.folderPath)
: entry.baseDirectory;
return {
...model,
@@ -2037,7 +2021,7 @@ export const missingBrowserMethods = {
const modelCount = countTreeModels(folder);
const shouldExpand = Boolean(filter) || expandedSet.has(stateKey);
const folderFullPath = group.baseDirectory
? joinLocalModelPath(group.baseDirectory, folder.path)
? joinPathPreservingStyle(group.baseDirectory, folder.path)
: '';
const context = buildLocalFolderContext(folderFullPath, folder.path, group.category);
const contextAttrs = getLocalPathContextAttrs(context, folderFullPath);