fix: normalize Windows backslash paths consistently in frontend path joins

This commit is contained in:
Dariusz L
2026-07-24 21:35:38 +02:00
parent 6ff9db2af6
commit 486ff262f1
4 changed files with 37 additions and 9 deletions
+3 -3
View File
@@ -30,9 +30,9 @@ def _normalized_absolute_path(path_value: str) -> str:
def normalize_file_manager_path(path_value: Any) -> str:
"""Normalize a local path and recover UI-style separators when necessary.
The frontend uses backslashes as its canonical subfolder separator. On
POSIX, a backslash is a valid filename character, so the literal path is
always checked first. It is interpreted as a separator only when the
Legacy frontend state may contain backslashes as subfolder separators. On
POSIX, a backslash is also a valid filename character, so the literal path
is always checked first. It is interpreted as a separator only when the
literal path does not exist and the converted path does.
"""
if not isinstance(path_value, (str, os.PathLike)):
@@ -927,6 +927,22 @@ test('portable subfolder paths use forward slashes and join to the host path sty
joinLocalPath.call(dialog, 'C:\\models\\loras', 'Pony/Styles'),
'C:\\models\\loras\\Pony\\Styles'
);
assert.equal(
joinLocalPath.call(dialog, '/', 'models\\Pony'),
'/models/Pony'
);
assert.equal(
joinLocalPath.call(dialog, '/models/literal\\name', 'Pony\\Styles'),
'/models/literal\\name/Pony/Styles'
);
assert.equal(
joinLocalPath.call(dialog, 'C:\\', 'Pony/Styles'),
'C:\\Pony\\Styles'
);
assert.equal(
joinLocalPath.call(dialog, '\\\\server\\share\\', 'Pony/Styles'),
'\\\\server\\share\\Pony\\Styles'
);
});
test('download subfolder tooltip identifies a folder taken from the workflow model path', () => {
@@ -1004,13 +1004,19 @@ export const downloadTargetMethods = {
},
joinLocalPath(basePath = '', relativePath = '') {
const base = String(basePath || '').replace(/[\/\\]+$/, '');
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 separator = base.includes('\\') ? '\\' : '/';
const normalizedRelative = relative.replace(/[\/\\]+/g, separator);
return `${base}${separator}${normalizedRelative}`;
const joiner = base.endsWith(separator) ? '' : separator;
return `${base}${joiner}${normalizedRelative}`;
},
getDownloadTargetFolderContext(category = '', subfolder = '', baseDirectory = '') {
@@ -1533,12 +1533,18 @@ export const missingBrowserMethods = {
if (typeof this.joinLocalPath === 'function') {
return this.joinLocalPath(basePath, relativePath);
}
const base = String(basePath || '').replace(/[\/\\]+$/, '');
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 separator = base.includes('\\') ? '\\' : '/';
return `${base}${separator}${relative.replace(/[\/\\]+/g, separator)}`;
const joiner = base.endsWith(separator) ? '' : separator;
return `${base}${joiner}${relative.replace(/[\/\\]+/g, separator)}`;
};
const buildLocalFolderContext = (folderPath = '', name = 'Folder', category = '') => {
const path = String(folderPath || '').trim();