From 7b4dc85e05d883f452e7dd61f1c4cbd91ec728fb Mon Sep 17 00:00:00 2001 From: IgorHaidukov Date: Fri, 9 Oct 2026 20:03:46 +0200 Subject: [PATCH 1/3] fix: return relative paths in search results --- .../java/cloudpage/service/FolderService.java | 9 ++++++--- .../cloudpage/service/FolderServiceTest.java | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/cloudpage/service/FolderService.java b/backend/src/main/java/cloudpage/service/FolderService.java index 6cfa24ae..ebbd3f07 100644 --- a/backend/src/main/java/cloudpage/service/FolderService.java +++ b/backend/src/main/java/cloudpage/service/FolderService.java @@ -62,6 +62,7 @@ public List searchInFolder( SearchFilter filter) throws IOException { + Path rootReal = Paths.get(rootPath).toRealPath().normalize(); Path folder = Paths.get(rootPath, folderPath).normalize(); validatePath(rootPath, folder); @@ -78,7 +79,7 @@ public List searchInFolder( return stream .filter(p -> !p.equals(folder)) .filter(p -> !p.startsWith(trashDir)) - .map(p -> createSearchResult(p, lowerQuery)) + .map(p -> createSearchResult(p, lowerQuery, rootReal)) .filter(r -> r.getScore() >= minScore) .filter(r -> matchesFilter(r, effectiveFilter)) .sorted(searchComparator(effectiveFilter)) @@ -153,7 +154,7 @@ private Comparator searchComparator(SearchFilter filter) { * @return a {@link SearchResult} with the calculated similarity score * @throws FileNotFoundException if the file or folder metadata cannot be read */ - private SearchResult createSearchResult(Path path, String query) { + private SearchResult createSearchResult(Path path, String query, Path rootReal) { String name = path.getFileName().toString(); // Locale.ROOT prevents using the system's local language for case conversion String lowerName = name.toLowerCase(Locale.ROOT); @@ -167,10 +168,12 @@ private SearchResult createSearchResult(Path path, String query) { } try { + Path pathReal = resolvePathWithinRoot(rootReal, path); + String relativePath = toRelativePath(rootReal, pathReal); boolean isDir = Files.isDirectory(path); return new SearchResult( name, - path.toString(), + relativePath, isDir ? "folder" : "file", isDir ? null : Files.size(path), isDir ? null : Files.probeContentType(path), diff --git a/backend/src/test/java/cloudpage/service/FolderServiceTest.java b/backend/src/test/java/cloudpage/service/FolderServiceTest.java index 41dda251..1dcf58ca 100644 --- a/backend/src/test/java/cloudpage/service/FolderServiceTest.java +++ b/backend/src/test/java/cloudpage/service/FolderServiceTest.java @@ -715,6 +715,24 @@ void getFolderContentPage_folderWithContents_sizeIsRecursiveSum() throws IOExcep // ── searchInFolder: metadata filters & sorting ─────────────────────────── + @Test + void searchInFolder_returnsUserRelativePath() throws IOException { + Path documents = Files.createDirectory(tempDir.resolve("Documents")); + Files.writeString(documents.resolve("report.txt"), "w"); + + List results = + folderService.searchInFolder(tempDir.toString(), "", "report", 20, 0); + + SearchResult result = + results.stream() + .filter(r -> r.getName().equals("report.txt")) + .findFirst() + .orElseThrow(); + + assertEquals("report.txt", result.getName()); + assertEquals("Documents/report.txt", result.getPath()); + } + @Test void searchInFolder_filterByTypeFile_excludesFolders() throws IOException { Files.writeString(tempDir.resolve("report-file.txt"), "x"); From f0fcecf918bd4776fe8556bfdd9cfe567444c647 Mon Sep 17 00:00:00 2001 From: IgorHaidukov Date: Fri, 9 Oct 2026 21:01:01 +0200 Subject: [PATCH 2/3] fix: preserve lexical paths for search symlinks --- .../java/cloudpage/service/FolderService.java | 4 ++-- .../cloudpage/service/FolderServiceTest.java | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/cloudpage/service/FolderService.java b/backend/src/main/java/cloudpage/service/FolderService.java index ebbd3f07..07532386 100644 --- a/backend/src/main/java/cloudpage/service/FolderService.java +++ b/backend/src/main/java/cloudpage/service/FolderService.java @@ -168,8 +168,8 @@ private SearchResult createSearchResult(Path path, String query, Path rootReal) } try { - Path pathReal = resolvePathWithinRoot(rootReal, path); - String relativePath = toRelativePath(rootReal, pathReal); + Path lexicalPath = path.toAbsolutePath().normalize(); + String relativePath = toRelativePath(rootReal, lexicalPath); boolean isDir = Files.isDirectory(path); return new SearchResult( name, diff --git a/backend/src/test/java/cloudpage/service/FolderServiceTest.java b/backend/src/test/java/cloudpage/service/FolderServiceTest.java index 1dcf58ca..876904ea 100644 --- a/backend/src/test/java/cloudpage/service/FolderServiceTest.java +++ b/backend/src/test/java/cloudpage/service/FolderServiceTest.java @@ -733,6 +733,29 @@ void searchInFolder_returnsUserRelativePath() throws IOException { assertEquals("Documents/report.txt", result.getPath()); } + @Test + void searchInFolder_symlinkOutsideRoot_doesNotFailSearch() throws IOException { + Path target = Files.createTempFile("outside-target", ".txt"); + Path link = tempDir.resolve("report-link"); + + try { + Files.createSymbolicLink(link, target); + } catch (UnsupportedOperationException | IOException | SecurityException exception) { + assumeTrue(false, "Symbolic links are not available: " + exception.getMessage()); + } + + List results = + folderService.searchInFolder(tempDir.toString(), "", "report", 20, 0); + + SearchResult result = + results.stream() + .filter(r -> r.getName().equals("report-link")) + .findFirst() + .orElseThrow(); + + assertEquals("report-link", result.getPath()); + } + @Test void searchInFolder_filterByTypeFile_excludesFolders() throws IOException { Files.writeString(tempDir.resolve("report-file.txt"), "x"); From 76650bd0c3d3d4b2c1d813e820e14296df12e083 Mon Sep 17 00:00:00 2001 From: IgorHaidukov Date: Fri, 9 Oct 2026 21:38:11 +0200 Subject: [PATCH 3/3] fix: use lexical root for search paths --- .../java/cloudpage/service/FolderService.java | 8 ++--- .../cloudpage/service/FolderServiceTest.java | 36 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/cloudpage/service/FolderService.java b/backend/src/main/java/cloudpage/service/FolderService.java index 07532386..9b884f70 100644 --- a/backend/src/main/java/cloudpage/service/FolderService.java +++ b/backend/src/main/java/cloudpage/service/FolderService.java @@ -62,7 +62,7 @@ public List searchInFolder( SearchFilter filter) throws IOException { - Path rootReal = Paths.get(rootPath).toRealPath().normalize(); + Path rootAbsolute = Paths.get(rootPath).toAbsolutePath().normalize(); Path folder = Paths.get(rootPath, folderPath).normalize(); validatePath(rootPath, folder); @@ -79,7 +79,7 @@ public List searchInFolder( return stream .filter(p -> !p.equals(folder)) .filter(p -> !p.startsWith(trashDir)) - .map(p -> createSearchResult(p, lowerQuery, rootReal)) + .map(p -> createSearchResult(p, lowerQuery, rootAbsolute)) .filter(r -> r.getScore() >= minScore) .filter(r -> matchesFilter(r, effectiveFilter)) .sorted(searchComparator(effectiveFilter)) @@ -154,7 +154,7 @@ private Comparator searchComparator(SearchFilter filter) { * @return a {@link SearchResult} with the calculated similarity score * @throws FileNotFoundException if the file or folder metadata cannot be read */ - private SearchResult createSearchResult(Path path, String query, Path rootReal) { + private SearchResult createSearchResult(Path path, String query, Path rootAbsolute) { String name = path.getFileName().toString(); // Locale.ROOT prevents using the system's local language for case conversion String lowerName = name.toLowerCase(Locale.ROOT); @@ -169,7 +169,7 @@ private SearchResult createSearchResult(Path path, String query, Path rootReal) try { Path lexicalPath = path.toAbsolutePath().normalize(); - String relativePath = toRelativePath(rootReal, lexicalPath); + String relativePath = toRelativePath(rootAbsolute, lexicalPath); boolean isDir = Files.isDirectory(path); return new SearchResult( name, diff --git a/backend/src/test/java/cloudpage/service/FolderServiceTest.java b/backend/src/test/java/cloudpage/service/FolderServiceTest.java index 876904ea..73d6d7a6 100644 --- a/backend/src/test/java/cloudpage/service/FolderServiceTest.java +++ b/backend/src/test/java/cloudpage/service/FolderServiceTest.java @@ -724,10 +724,7 @@ void searchInFolder_returnsUserRelativePath() throws IOException { folderService.searchInFolder(tempDir.toString(), "", "report", 20, 0); SearchResult result = - results.stream() - .filter(r -> r.getName().equals("report.txt")) - .findFirst() - .orElseThrow(); + results.stream().filter(r -> r.getName().equals("report.txt")).findFirst().orElseThrow(); assertEquals("report.txt", result.getName()); assertEquals("Documents/report.txt", result.getPath()); @@ -748,14 +745,37 @@ void searchInFolder_symlinkOutsideRoot_doesNotFailSearch() throws IOException { folderService.searchInFolder(tempDir.toString(), "", "report", 20, 0); SearchResult result = - results.stream() - .filter(r -> r.getName().equals("report-link")) - .findFirst() - .orElseThrow(); + results.stream().filter(r -> r.getName().equals("report-link")).findFirst().orElseThrow(); assertEquals("report-link", result.getPath()); } + @Test + void searchInFolder_symlinkedRootAncestor_returnsRelativePath() throws IOException { + Path realBase = Files.createDirectory(tempDir.resolve("real-base")); + Path userRoot = Files.createDirectory(realBase.resolve("user-root")); + Path documents = Files.createDirectory(userRoot.resolve("Documents")); + Files.writeString(documents.resolve("report.txt"), "w"); + + Path linkedBase = tempDir.resolve("linked-base"); + + try { + Files.createSymbolicLink(linkedBase, realBase); + } catch (UnsupportedOperationException | IOException | SecurityException exception) { + assumeTrue(false, "Symbolic links are not available: " + exception.getMessage()); + } + + Path lexicalRoot = linkedBase.resolve("user-root"); + + List results = + folderService.searchInFolder(lexicalRoot.toString(), "", "report", 20, 0); + + SearchResult result = + results.stream().filter(r -> r.getName().equals("report.txt")).findFirst().orElseThrow(); + + assertEquals("Documents/report.txt", result.getPath()); + } + @Test void searchInFolder_filterByTypeFile_excludesFolders() throws IOException { Files.writeString(tempDir.resolve("report-file.txt"), "x");