diff --git a/core/file_manager.py b/core/file_manager.py index 3190314..d51a0ea 100644 --- a/core/file_manager.py +++ b/core/file_manager.py @@ -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)): diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 2b8ddf2..4a9e0cc 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -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', () => { diff --git a/web/resolver/search/download_target_methods.js b/web/resolver/search/download_target_methods.js index dca388e..b8e78b7 100644 --- a/web/resolver/search/download_target_methods.js +++ b/web/resolver/search/download_target_methods.js @@ -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 = '') { diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 05f8b64..fef27d3 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -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();