From 2472fbcd30502606feb11fbc8b8dc4f3803e6641 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 27 Jun 2026 08:01:26 +0200 Subject: [PATCH] fix: normalize recursive listing paths to forward slashes (#6003) --- .../utils/__tests__/check-conflict.test.ts | 48 +++++++++++++++++++ frontend/src/utils/upload.ts | 7 +-- http/resource.go | 5 +- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/frontend/src/utils/__tests__/check-conflict.test.ts b/frontend/src/utils/__tests__/check-conflict.test.ts index 37a4e69b..dcf3193c 100644 --- a/frontend/src/utils/__tests__/check-conflict.test.ts +++ b/frontend/src/utils/__tests__/check-conflict.test.ts @@ -220,4 +220,52 @@ describe("checkConflict", () => { expect(conflicts).toHaveLength(0); }); + + // Regression for #5980: a FileBrowser server running on Windows returns + // backslash-separated paths from the recursive listing. Without normalizing + // them, the prefix strip and key lookup never match, so the conflict modal is + // skipped and the backend returns a bare 409. + it("detects a conflict for backslash-separated server paths (Windows)", async () => { + vi.mocked(api.fetchAll).mockResolvedValue([ + { + path: "\\target\\file.txt", + name: "file.txt", + size: 10, + modified: "2026-06-04T00:00:00Z", + isDir: false, + }, + ]); + + const conflicts = await checkConflict( + [moveItem("file.txt", "/files/target/")], + "/files/target/" + ); + + expect(conflicts).toHaveLength(1); + }); + + it("detects nested conflicts for backslash-separated server paths (Windows)", async () => { + vi.mocked(api.fetchAll).mockResolvedValue([ + { + path: "\\target\\folder\\nested file.txt", + name: "nested file.txt", + size: 10, + modified: "2026-06-04T00:00:00Z", + isDir: false, + }, + ]); + + const files = [ + { + name: "nested file.txt", + size: 12, + isDir: false, + fullPath: "folder/nested file.txt", + }, + ]; + + const conflicts = await checkConflict(files, "/files/target/"); + + expect(conflicts).toHaveLength(1); + }); }); diff --git a/frontend/src/utils/upload.ts b/frontend/src/utils/upload.ts index c4a9aab4..1c0b591e 100644 --- a/frontend/src/utils/upload.ts +++ b/frontend/src/utils/upload.ts @@ -62,9 +62,10 @@ export async function checkConflict( const normBase = removePrefix(basePath).replace(/\/+$/, ""); const serverMap = new Map(); for (const entry of serverEntries) { - const rel = entry.path.startsWith(normBase) - ? entry.path.slice(normBase.length) - : entry.path; + // A Windows server may return OS-native backslash separators; normalize to + // forward slashes so the prefix strip and key lookup line up. + const path = entry.path.replace(/\\/g, "/"); + const rel = path.startsWith(normBase) ? path.slice(normBase.length) : path; serverMap.set(rel.replace(/^\/+/, ""), entry); } diff --git a/http/resource.go b/http/resource.go index 728bad73..eda4e403 100644 --- a/http/resource.go +++ b/http/resource.go @@ -424,7 +424,10 @@ var resourceGetRecursiveHandler = withUser(func(w http.ResponseWriter, r *http.R } entries = append(entries, RecursiveEntry{ - Path: fPath, + // afero.Walk joins paths with the OS separator, so on Windows fPath + // uses backslashes. The web API contract is forward slashes, so + // normalize it here (mirrors search/search.go). + Path: filepath.ToSlash(fPath), Name: info.Name(), Size: info.Size(), ModTime: info.ModTime(),