diff --git a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java index 1fcf5e0..2d81ab3 100644 --- a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java +++ b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java @@ -77,10 +77,8 @@ public class FileOperations { while (true) { try { Path sourcePath = source.toPath(); - System.out.println("[DEBUG] Processing item: " + sourcePath + " (isLink: " + Files.isSymbolicLink(sourcePath) + ")"); if (Files.isSymbolicLink(sourcePath)) { SymlinkResponse sRes = getSymlinkResponse(source, callback, globalSymlinkResponse); - System.out.println("[DEBUG] Symlink response for " + source.getName() + ": " + sRes); if (sRes == SymlinkResponse.CANCEL) { return; } @@ -91,33 +89,27 @@ public class FileOperations { // If FOLLOW, sRes will be FOLLOW or FOLLOW_ALL if (Files.exists(sourcePath)) { if (Files.isDirectory(sourcePath)) { - System.out.println("[DEBUG] Following symlink as directory: " + sourcePath); copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null); // Count the symlink itself as processed to match countItems prediction currentItem[0]++; } else { - System.out.println("[DEBUG] Following symlink as file: " + sourcePath); copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback); } } else { // Link is broken, follow is impossible, copy as link - System.out.println("[DEBUG] Broken link encountered, copying as link: " + sourcePath); copySymlink(sourcePath, target.toPath(), totalItems, currentItem, callback); } break; } else if (source.isDirectory()) { - System.out.println("[DEBUG] Copying directory: " + sourcePath); copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null); // Directory itself counted in countItems but copyDirectory skips increment for root. // So we increment here. currentItem[0]++; } else { - System.out.println("[DEBUG] Copying file: " + sourcePath); copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback); } break; } catch (IOException e) { - System.out.println("[DEBUG] Error processing " + source.getName() + ": " + e.getMessage()); if (callback != null) { ErrorResponse res = callback.onError(source, e); if (res == ErrorResponse.ABORT) throw e; @@ -253,7 +245,6 @@ public class FileOperations { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE; Path targetFile = finalTarget.resolve(effectiveSource.relativize(file.toAbsolutePath().normalize())); - System.out.println("[DEBUG] Recursive visit (file): " + file + " (isLink: " + attrs.isSymbolicLink() + ")"); if (Files.exists(targetFile, LinkOption.NOFOLLOW_LINKS)) { if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE; @@ -648,36 +639,59 @@ public class FileOperations { } private static void searchRecursive(Path directory, String patternLower, Pattern filenameRegex, Pattern contentPattern, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException { + if (callback != null && callback.isCancelled()) return; + + // Use absolute path for reliable prefix checking + Path absolutePath = directory.toAbsolutePath().normalize(); + String pathStr = absolutePath.toString(); + + callback.onProgress(pathStr); + + // Skip troublesome virtual filesystems and device nodes + if (pathStr.startsWith("/proc") || pathStr.startsWith("/sys") || pathStr.startsWith("/dev")) { + return; + } + try (DirectoryStream stream = Files.newDirectoryStream(directory)) { for (Path entry : stream) { - if (Files.isDirectory(entry)) { - if (recursive) { - searchRecursive(entry, patternLower, filenameRegex, contentPattern, recursive, searchArchives, callback); - } - } else { - File file = entry.toFile(); - boolean nameMatched = true; - if (patternLower != null && !patternLower.isEmpty()) { - nameMatched = matchName(file.getName(), patternLower, filenameRegex); + if (callback != null && callback.isCancelled()) return; + try { + // Always skip symbolic links during search to prevent infinite loops and hangs on special files + if (Files.isSymbolicLink(entry)) { + continue; } - boolean contentMatched = true; - if (nameMatched && contentPattern != null) { - contentMatched = fileMatchesContent(entry, contentPattern); - } + if (Files.isDirectory(entry)) { + if (recursive) { + searchRecursive(entry, patternLower, filenameRegex, contentPattern, recursive, searchArchives, callback); + } + } else { + File file = entry.toFile(); + boolean nameMatched = true; + if (patternLower != null && !patternLower.isEmpty()) { + nameMatched = matchName(file.getName(), patternLower, filenameRegex); + } - if (nameMatched && contentMatched) { - callback.onFileFound(file, null); - } + boolean contentMatched = true; + if (nameMatched && contentPattern != null) { + contentMatched = fileMatchesContent(entry, contentPattern); + } - // SEARCH IN ARCHIVES - if (searchArchives && isArchiveFile(file)) { - searchInArchiveCombined(file, patternLower, filenameRegex, contentPattern, callback); + if (nameMatched && contentMatched) { + callback.onFileFound(file, null); + } + + // SEARCH IN ARCHIVES + if (searchArchives && isArchiveFile(file)) { + searchInArchiveCombined(file, patternLower, filenameRegex, contentPattern, callback); + } } + } catch (AccessDeniedException e) { + // Silently skip } } } catch (AccessDeniedException e) { - // Ignore directories without access + // Silently skip } } @@ -699,7 +713,7 @@ public class FileOperations { } } } catch (IOException ex) { - // Skip files that cannot be read as text + // Silently skip } return false; } @@ -783,12 +797,14 @@ public class FileOperations { } private static void searchInArchiveCombined(File archive, String patternLower, Pattern filenameRegex, Pattern contentPattern, SearchCallback callback) { + if (callback != null && callback.isCancelled()) return; String name = archive.getName().toLowerCase(); try { if (name.endsWith(".zip") || name.endsWith(".jar")) { try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { + if (callback.isCancelled()) return; if (!entry.isDirectory()) { boolean nameMatched = true; if (patternLower != null && !patternLower.isEmpty()) { @@ -812,6 +828,7 @@ public class FileOperations { try (org.apache.commons.compress.archivers.tar.TarArchiveInputStream tais = new org.apache.commons.compress.archivers.tar.TarArchiveInputStream(is)) { org.apache.commons.compress.archivers.tar.TarArchiveEntry entry; while ((entry = tais.getNextTarEntry()) != null) { + if (callback.isCancelled()) return; if (!entry.isDirectory()) { boolean nameMatched = true; if (patternLower != null && !patternLower.isEmpty()) { @@ -831,6 +848,7 @@ public class FileOperations { try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) { org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry; while ((entry = sevenZFile.getNextEntry()) != null) { + if (callback.isCancelled()) return; if (!entry.isDirectory()) { boolean nameMatched = true; if (patternLower != null && !patternLower.isEmpty()) { @@ -858,6 +876,7 @@ public class FileOperations { } else if (name.endsWith(".rar")) { try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) { for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) { + if (callback.isCancelled()) return; if (!fh.isDirectory()) { boolean nameMatched = true; if (patternLower != null && !patternLower.isEmpty()) { @@ -876,7 +895,9 @@ public class FileOperations { } } } - } catch (Exception ignore) {} + } catch (Exception e) { + // Silently skip archive errors during search + } } @@ -1063,5 +1084,7 @@ public class FileOperations { */ public interface SearchCallback { void onFileFound(File file, String virtualPath); + default boolean isCancelled() { return false; } + default void onProgress(String status) {} } } diff --git a/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java b/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java index ef853bf..a054597 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java +++ b/src/main/java/cz/kamma/kfmanager/ui/SearchDialog.java @@ -26,6 +26,7 @@ public class SearchDialog extends JDialog { private JTable resultsTable; private ResultsTableModel tableModel; private JButton searchButton; + private JButton stopButton; private JButton cancelButton; private JButton viewButton; private JButton editButton; @@ -263,6 +264,14 @@ public class SearchDialog extends JDialog { searchButton = new JButton("Search"); searchButton.addActionListener(e -> performSearch()); + stopButton = new JButton("Cancel"); + stopButton.setEnabled(false); + stopButton.addActionListener(e -> { + searching = false; + stopButton.setEnabled(false); + statusLabel.setText("Cancelling..."); + }); + viewButton = new JButton("View"); viewButton.setEnabled(false); viewButton.addActionListener(e -> viewSelectedFile()); @@ -272,12 +281,16 @@ public class SearchDialog extends JDialog { editButton.addActionListener(e -> editSelectedFile()); cancelButton = new JButton("Close"); - cancelButton.addActionListener(e -> dispose()); + cancelButton.addActionListener(e -> { + searching = false; + dispose(); + }); JButton openButton = new JButton("Open location"); openButton.addActionListener(e -> openSelectedFile()); buttonPanel.add(searchButton); + buttonPanel.add(stopButton); buttonPanel.add(viewButton); buttonPanel.add(editButton); buttonPanel.add(openButton); @@ -494,6 +507,7 @@ public class SearchDialog extends JDialog { tableModel.clear(); searchButton.setEnabled(false); + stopButton.setEnabled(true); searching = true; // Persist the chosen patterns into history @@ -544,11 +558,25 @@ public class SearchDialog extends JDialog { // Spustit vyhledávání v samostatném vlákně SwingWorker worker = new SwingWorker() { + private String currentSearchPath = ""; + @Override protected Void doInBackground() throws Exception { - FileOperations.search(searchDirectory, finalNamePat, finalContentPat, recursiveCheckBox.isSelected(), searchArchives, (file, virtualPath) -> { - if (!searching) return; - publish(new Object[]{file, virtualPath}); + FileOperations.search(searchDirectory, finalNamePat, finalContentPat, recursiveCheckBox.isSelected(), searchArchives, new FileOperations.SearchCallback() { + @Override + public void onFileFound(File file, String virtualPath) { + publish(new Object[]{"file", file, virtualPath}); + } + + @Override + public boolean isCancelled() { + return !searching; + } + + @Override + public void onProgress(String status) { + publish(new Object[]{"progress", status}); + } }); return null; } @@ -556,25 +584,41 @@ public class SearchDialog extends JDialog { @Override protected void process(List chunks) { for (Object[] chunk : chunks) { - File file = (File) chunk[0]; - String virtualPath = (String) chunk[1]; - tableModel.addResult(new FileItem(file, virtualPath)); - // update found count and status - foundCount++; - statusLabel.setText("Found " + foundCount + " — searching..."); - // If this is the first found file, select it and focus the table - if (foundCount == 1) { - try { - resultsTable.setRowSelectionInterval(0, 0); - resultsTable.requestFocusInWindow(); - } catch (Exception ignore) {} + String type = (String) chunk[0]; + if ("file".equals(type)) { + File file = (File) chunk[1]; + String virtualPath = (String) chunk[2]; + tableModel.addResult(new FileItem(file, virtualPath)); + foundCount++; + updateStatusLabel(currentSearchPath); + if (foundCount == 1) { + try { + resultsTable.setRowSelectionInterval(0, 0); + resultsTable.requestFocusInWindow(); + } catch (Exception ignore) {} + } + } else if ("progress".equals(type)) { + currentSearchPath = (String) chunk[1]; + updateStatusLabel(currentSearchPath); } } } + + private void updateStatusLabel(String currentPath) { + String text = "Found " + foundCount; + if (currentPath != null && !currentPath.isEmpty()) { + text += " — " + currentPath; + } else { + text += " — searching..."; + } + statusLabel.setText(text); + statusLabel.setToolTipText(currentPath); + } @Override protected void done() { searchButton.setEnabled(true); + stopButton.setEnabled(false); searching = false; // finalize status statusProgressBar.setIndeterminate(false);