search improved

This commit is contained in:
rdavidek 2026-01-27 22:47:22 +01:00
parent 24a0ab7451
commit 0867fa0dab
2 changed files with 114 additions and 47 deletions

View File

@ -77,10 +77,8 @@ public class FileOperations {
while (true) { while (true) {
try { try {
Path sourcePath = source.toPath(); Path sourcePath = source.toPath();
System.out.println("[DEBUG] Processing item: " + sourcePath + " (isLink: " + Files.isSymbolicLink(sourcePath) + ")");
if (Files.isSymbolicLink(sourcePath)) { if (Files.isSymbolicLink(sourcePath)) {
SymlinkResponse sRes = getSymlinkResponse(source, callback, globalSymlinkResponse); SymlinkResponse sRes = getSymlinkResponse(source, callback, globalSymlinkResponse);
System.out.println("[DEBUG] Symlink response for " + source.getName() + ": " + sRes);
if (sRes == SymlinkResponse.CANCEL) { if (sRes == SymlinkResponse.CANCEL) {
return; return;
} }
@ -91,33 +89,27 @@ public class FileOperations {
// If FOLLOW, sRes will be FOLLOW or FOLLOW_ALL // If FOLLOW, sRes will be FOLLOW or FOLLOW_ALL
if (Files.exists(sourcePath)) { if (Files.exists(sourcePath)) {
if (Files.isDirectory(sourcePath)) { if (Files.isDirectory(sourcePath)) {
System.out.println("[DEBUG] Following symlink as directory: " + sourcePath);
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null); copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
// Count the symlink itself as processed to match countItems prediction // Count the symlink itself as processed to match countItems prediction
currentItem[0]++; currentItem[0]++;
} else { } else {
System.out.println("[DEBUG] Following symlink as file: " + sourcePath);
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback); copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
} }
} else { } else {
// Link is broken, follow is impossible, copy as link // 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); copySymlink(sourcePath, target.toPath(), totalItems, currentItem, callback);
} }
break; break;
} else if (source.isDirectory()) { } else if (source.isDirectory()) {
System.out.println("[DEBUG] Copying directory: " + sourcePath);
copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null); copyDirectory(sourcePath, target.toPath(), totalItems, currentItem, callback, globalResponse, globalSymlinkResponse, null);
// Directory itself counted in countItems but copyDirectory skips increment for root. // Directory itself counted in countItems but copyDirectory skips increment for root.
// So we increment here. // So we increment here.
currentItem[0]++; currentItem[0]++;
} else { } else {
System.out.println("[DEBUG] Copying file: " + sourcePath);
copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback); copyFileWithProgress(sourcePath, target.toPath(), totalItems, currentItem, callback);
} }
break; break;
} catch (IOException e) { } catch (IOException e) {
System.out.println("[DEBUG] Error processing " + source.getName() + ": " + e.getMessage());
if (callback != null) { if (callback != null) {
ErrorResponse res = callback.onError(source, e); ErrorResponse res = callback.onError(source, e);
if (res == ErrorResponse.ABORT) throw e; if (res == ErrorResponse.ABORT) throw e;
@ -253,7 +245,6 @@ public class FileOperations {
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE; if (callback != null && callback.isCancelled()) return FileVisitResult.TERMINATE;
Path targetFile = finalTarget.resolve(effectiveSource.relativize(file.toAbsolutePath().normalize())); 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 (Files.exists(targetFile, LinkOption.NOFOLLOW_LINKS)) {
if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE; if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) return FileVisitResult.CONTINUE;
@ -648,8 +639,28 @@ public class FileOperations {
} }
private static void searchRecursive(Path directory, String patternLower, Pattern filenameRegex, Pattern contentPattern, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException { 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<Path> stream = Files.newDirectoryStream(directory)) { try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path entry : stream) { for (Path entry : stream) {
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;
}
if (Files.isDirectory(entry)) { if (Files.isDirectory(entry)) {
if (recursive) { if (recursive) {
searchRecursive(entry, patternLower, filenameRegex, contentPattern, recursive, searchArchives, callback); searchRecursive(entry, patternLower, filenameRegex, contentPattern, recursive, searchArchives, callback);
@ -675,9 +686,12 @@ public class FileOperations {
searchInArchiveCombined(file, patternLower, filenameRegex, contentPattern, callback); searchInArchiveCombined(file, patternLower, filenameRegex, contentPattern, callback);
} }
} }
} catch (AccessDeniedException e) {
// Silently skip
}
} }
} catch (AccessDeniedException e) { } catch (AccessDeniedException e) {
// Ignore directories without access // Silently skip
} }
} }
@ -699,7 +713,7 @@ public class FileOperations {
} }
} }
} catch (IOException ex) { } catch (IOException ex) {
// Skip files that cannot be read as text // Silently skip
} }
return false; return false;
} }
@ -783,12 +797,14 @@ public class FileOperations {
} }
private static void searchInArchiveCombined(File archive, String patternLower, Pattern filenameRegex, Pattern contentPattern, SearchCallback callback) { 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(); String name = archive.getName().toLowerCase();
try { try {
if (name.endsWith(".zip") || name.endsWith(".jar")) { if (name.endsWith(".zip") || name.endsWith(".jar")) {
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) { try (ZipInputStream zis = new ZipInputStream(new FileInputStream(archive))) {
ZipEntry entry; ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) { while ((entry = zis.getNextEntry()) != null) {
if (callback.isCancelled()) return;
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
boolean nameMatched = true; boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) { 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)) { 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; org.apache.commons.compress.archivers.tar.TarArchiveEntry entry;
while ((entry = tais.getNextTarEntry()) != null) { while ((entry = tais.getNextTarEntry()) != null) {
if (callback.isCancelled()) return;
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
boolean nameMatched = true; boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) { 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)) { 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; org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
while ((entry = sevenZFile.getNextEntry()) != null) { while ((entry = sevenZFile.getNextEntry()) != null) {
if (callback.isCancelled()) return;
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
boolean nameMatched = true; boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) { if (patternLower != null && !patternLower.isEmpty()) {
@ -858,6 +876,7 @@ public class FileOperations {
} else if (name.endsWith(".rar")) { } else if (name.endsWith(".rar")) {
try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) { try (com.github.junrar.Archive rar = new com.github.junrar.Archive(archive)) {
for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) { for (com.github.junrar.rarfile.FileHeader fh : rar.getFileHeaders()) {
if (callback.isCancelled()) return;
if (!fh.isDirectory()) { if (!fh.isDirectory()) {
boolean nameMatched = true; boolean nameMatched = true;
if (patternLower != null && !patternLower.isEmpty()) { 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 { public interface SearchCallback {
void onFileFound(File file, String virtualPath); void onFileFound(File file, String virtualPath);
default boolean isCancelled() { return false; }
default void onProgress(String status) {}
} }
} }

View File

@ -26,6 +26,7 @@ public class SearchDialog extends JDialog {
private JTable resultsTable; private JTable resultsTable;
private ResultsTableModel tableModel; private ResultsTableModel tableModel;
private JButton searchButton; private JButton searchButton;
private JButton stopButton;
private JButton cancelButton; private JButton cancelButton;
private JButton viewButton; private JButton viewButton;
private JButton editButton; private JButton editButton;
@ -263,6 +264,14 @@ public class SearchDialog extends JDialog {
searchButton = new JButton("Search"); searchButton = new JButton("Search");
searchButton.addActionListener(e -> performSearch()); 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 = new JButton("View");
viewButton.setEnabled(false); viewButton.setEnabled(false);
viewButton.addActionListener(e -> viewSelectedFile()); viewButton.addActionListener(e -> viewSelectedFile());
@ -272,12 +281,16 @@ public class SearchDialog extends JDialog {
editButton.addActionListener(e -> editSelectedFile()); editButton.addActionListener(e -> editSelectedFile());
cancelButton = new JButton("Close"); cancelButton = new JButton("Close");
cancelButton.addActionListener(e -> dispose()); cancelButton.addActionListener(e -> {
searching = false;
dispose();
});
JButton openButton = new JButton("Open location"); JButton openButton = new JButton("Open location");
openButton.addActionListener(e -> openSelectedFile()); openButton.addActionListener(e -> openSelectedFile());
buttonPanel.add(searchButton); buttonPanel.add(searchButton);
buttonPanel.add(stopButton);
buttonPanel.add(viewButton); buttonPanel.add(viewButton);
buttonPanel.add(editButton); buttonPanel.add(editButton);
buttonPanel.add(openButton); buttonPanel.add(openButton);
@ -494,6 +507,7 @@ public class SearchDialog extends JDialog {
tableModel.clear(); tableModel.clear();
searchButton.setEnabled(false); searchButton.setEnabled(false);
stopButton.setEnabled(true);
searching = true; searching = true;
// Persist the chosen patterns into history // Persist the chosen patterns into history
@ -544,11 +558,25 @@ public class SearchDialog extends JDialog {
// Spustit vyhledávání v samostatném vlákně // Spustit vyhledávání v samostatném vlákně
SwingWorker<Void, Object[]> worker = new SwingWorker<Void, Object[]>() { SwingWorker<Void, Object[]> worker = new SwingWorker<Void, Object[]>() {
private String currentSearchPath = "";
@Override @Override
protected Void doInBackground() throws Exception { protected Void doInBackground() throws Exception {
FileOperations.search(searchDirectory, finalNamePat, finalContentPat, recursiveCheckBox.isSelected(), searchArchives, (file, virtualPath) -> { FileOperations.search(searchDirectory, finalNamePat, finalContentPat, recursiveCheckBox.isSelected(), searchArchives, new FileOperations.SearchCallback() {
if (!searching) return; @Override
publish(new Object[]{file, virtualPath}); 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; return null;
} }
@ -556,25 +584,41 @@ public class SearchDialog extends JDialog {
@Override @Override
protected void process(List<Object[]> chunks) { protected void process(List<Object[]> chunks) {
for (Object[] chunk : chunks) { for (Object[] chunk : chunks) {
File file = (File) chunk[0]; String type = (String) chunk[0];
String virtualPath = (String) chunk[1]; if ("file".equals(type)) {
File file = (File) chunk[1];
String virtualPath = (String) chunk[2];
tableModel.addResult(new FileItem(file, virtualPath)); tableModel.addResult(new FileItem(file, virtualPath));
// update found count and status
foundCount++; foundCount++;
statusLabel.setText("Found " + foundCount + " — searching..."); updateStatusLabel(currentSearchPath);
// If this is the first found file, select it and focus the table
if (foundCount == 1) { if (foundCount == 1) {
try { try {
resultsTable.setRowSelectionInterval(0, 0); resultsTable.setRowSelectionInterval(0, 0);
resultsTable.requestFocusInWindow(); resultsTable.requestFocusInWindow();
} catch (Exception ignore) {} } 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 @Override
protected void done() { protected void done() {
searchButton.setEnabled(true); searchButton.setEnabled(true);
stopButton.setEnabled(false);
searching = false; searching = false;
// finalize status // finalize status
statusProgressBar.setIndeterminate(false); statusProgressBar.setIndeterminate(false);