diff --git a/backend/src/main/java/cloudpage/service/FolderService.java b/backend/src/main/java/cloudpage/service/FolderService.java index 6cfa24ae..9b884f70 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 rootAbsolute = Paths.get(rootPath).toAbsolutePath().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, rootAbsolute)) .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 rootAbsolute) { 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 lexicalPath = path.toAbsolutePath().normalize(); + String relativePath = toRelativePath(rootAbsolute, lexicalPath); 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..73d6d7a6 100644 --- a/backend/src/test/java/cloudpage/service/FolderServiceTest.java +++ b/backend/src/test/java/cloudpage/service/FolderServiceTest.java @@ -715,6 +715,67 @@ 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_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_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");