backgroun file operations, error handling

This commit is contained in:
rdavidek 2026-01-19 21:45:01 +01:00
parent 7ba25ed50a
commit af98d9f112
6 changed files with 264 additions and 1081 deletions

View File

@ -11,7 +11,7 @@ import java.awt.event.KeyEvent;
*/ */
public class MainApp { public class MainApp {
public static final String APP_VERSION = "0.0.4"; public static final String APP_VERSION = "0.0.5";
public static void main(String[] args) { public static void main(String[] args) {
// Set application name for X11/Wayland WM_CLASS // Set application name for X11/Wayland WM_CLASS

View File

@ -49,10 +49,24 @@ public class FileOperations {
} }
} }
if (source.isDirectory()) { while (true) {
copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback, globalResponse); try {
} else { if (source.isDirectory()) {
copyFileWithProgress(source.toPath(), target.toPath(), totalSize, currentCopied, callback); copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback, globalResponse);
} else {
copyFileWithProgress(source.toPath(), target.toPath(), totalSize, currentCopied, callback);
}
break;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(source, e);
if (res == ErrorResponse.ABORT) throw e;
if (res == ErrorResponse.RETRY) continue;
break;
} else {
throw e;
}
}
} }
} }
} }
@ -84,6 +98,7 @@ public class FileOperations {
} }
private static void copyFileWithProgress(Path source, Path target, long totalSize, long[] totalCopied, ProgressCallback callback) throws IOException { private static void copyFileWithProgress(Path source, Path target, long totalSize, long[] totalCopied, ProgressCallback callback) throws IOException {
long initialTotalCopied = totalCopied[0];
try (InputStream in = Files.newInputStream(source); try (InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)) { OutputStream out = Files.newOutputStream(target)) {
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
@ -96,6 +111,9 @@ public class FileOperations {
callback.onProgress(totalCopied[0], totalSize, source.getFileName().toString()); callback.onProgress(totalCopied[0], totalSize, source.getFileName().toString());
} }
} }
} catch (IOException e) {
totalCopied[0] = initialTotalCopied;
throw e;
} }
Files.setLastModifiedTime(target, Files.getLastModifiedTime(source)); Files.setLastModifiedTime(target, Files.getLastModifiedTime(source));
} }
@ -105,8 +123,20 @@ public class FileOperations {
@Override @Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = target.resolve(source.relativize(dir)); Path targetDir = target.resolve(source.relativize(dir));
Files.createDirectories(targetDir); while (true) {
return FileVisitResult.CONTINUE; try {
Files.createDirectories(targetDir);
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(dir.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.SKIP_SUBTREE;
}
throw e;
}
}
} }
@Override @Override
@ -128,8 +158,20 @@ public class FileOperations {
} }
} }
copyFileWithProgress(file, targetFile, totalSize, totalCopied, callback); while (true) {
return FileVisitResult.CONTINUE; try {
copyFileWithProgress(file, targetFile, totalSize, totalCopied, callback);
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(file.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.CONTINUE;
}
throw e;
}
}
} }
}); });
} }
@ -170,7 +212,21 @@ public class FileOperations {
callback.onProgress(current, total, source.getName()); callback.onProgress(current, total, source.getName());
} }
Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); while (true) {
try {
Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
break;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(source, e);
if (res == ErrorResponse.ABORT) throw e;
if (res == ErrorResponse.RETRY) continue;
break;
} else {
throw e;
}
}
}
} }
} }
@ -190,10 +246,24 @@ public class FileOperations {
callback.onProgress(current, total, file.getName()); callback.onProgress(current, total, file.getName());
} }
if (file.isDirectory()) { while (true) {
deleteDirectory(file.toPath()); try {
} else { if (file.isDirectory()) {
Files.delete(file.toPath()); deleteDirectory(file.toPath(), callback);
} else {
Files.delete(file.toPath());
}
break;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(file, e);
if (res == ErrorResponse.ABORT) throw e;
if (res == ErrorResponse.RETRY) continue;
break;
} else {
throw e;
}
}
} }
} }
} }
@ -219,18 +289,43 @@ public class FileOperations {
/** /**
* Delete directory recursively * Delete directory recursively
*/ */
private static void deleteDirectory(Path directory) throws IOException { private static void deleteDirectory(Path directory, ProgressCallback callback) throws IOException {
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() { Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file); while (true) {
return FileVisitResult.CONTINUE; try {
Files.delete(file);
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(file.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.CONTINUE;
}
throw e;
}
}
} }
@Override @Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); if (exc != null) throw exc;
return FileVisitResult.CONTINUE; while (true) {
try {
Files.delete(dir);
return FileVisitResult.CONTINUE;
} catch (IOException e) {
if (callback != null) {
ErrorResponse res = callback.onError(dir.toFile(), e);
if (res == ErrorResponse.ABORT) return FileVisitResult.TERMINATE;
if (res == ErrorResponse.RETRY) continue;
return FileVisitResult.CONTINUE;
}
throw e;
}
}
} }
}); });
} }
@ -572,10 +667,15 @@ public class FileOperations {
YES, NO, YES_TO_ALL, NO_TO_ALL, CANCEL YES, NO, YES_TO_ALL, NO_TO_ALL, CANCEL
} }
public enum ErrorResponse {
SKIP, RETRY, ABORT
}
public interface ProgressCallback { public interface ProgressCallback {
void onProgress(long current, long total, String currentFile); void onProgress(long current, long total, String currentFile);
default boolean isCancelled() { return false; } default boolean isCancelled() { return false; }
default OverwriteResponse confirmOverwrite(File file) { return OverwriteResponse.YES; } default OverwriteResponse confirmOverwrite(File file) { return OverwriteResponse.YES; }
default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; }
} }
/** /**

File diff suppressed because it is too large Load Diff

View File

@ -1387,6 +1387,35 @@ public class FilePanelTab extends JPanel {
public boolean isCancelled() { public boolean isCancelled() {
return progressDialog.isCancelled(); return progressDialog.isCancelled();
} }
@Override
public FileOperations.ErrorResponse onError(File file, Exception e) {
final FileOperations.ErrorResponse[] result = new FileOperations.ErrorResponse[1];
try {
SwingUtilities.invokeAndWait(() -> {
Object[] options = {"Skip", "Retry", "Abort"};
int n = JOptionPane.showOptionDialog(progressDialog,
"Error deleting file: " + file.getName() + "\n" + e.getMessage(),
"Error",
JOptionPane.DEFAULT_OPTION,
JOptionPane.ERROR_MESSAGE,
null,
options,
options[0]);
switch (n) {
case 0: result[0] = FileOperations.ErrorResponse.SKIP; break;
case 1: result[0] = FileOperations.ErrorResponse.RETRY; break;
default:
result[0] = FileOperations.ErrorResponse.ABORT;
progressDialog.cancel();
break;
}
});
} catch (Exception ex) {
result[0] = FileOperations.ErrorResponse.ABORT;
}
return result[0];
}
}); });
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
progressDialog.dispose(); progressDialog.dispose();
@ -1781,14 +1810,44 @@ public class FilePanelTab extends JPanel {
} }
return result[0]; return result[0];
} }
};
if (action == ClipboardService.ClipboardAction.CUT) { @Override
FileOperations.move(itemsToPaste, targetDir, callback); public FileOperations.ErrorResponse onError(File file, Exception e) {
} else { final FileOperations.ErrorResponse[] result = new FileOperations.ErrorResponse[1];
FileOperations.copy(itemsToPaste, targetDir, callback); try {
SwingUtilities.invokeAndWait(() -> {
Object[] options = {"Skip", "Retry", "Abort"};
int n = JOptionPane.showOptionDialog(progressDialog,
"Error operating on file: " + file.getName() + "\n" + e.getMessage(),
"Error",
JOptionPane.DEFAULT_OPTION,
JOptionPane.ERROR_MESSAGE,
null,
options,
options[0]);
switch (n) {
case 0: result[0] = FileOperations.ErrorResponse.SKIP; break;
case 1: result[0] = FileOperations.ErrorResponse.RETRY; break;
default:
result[0] = FileOperations.ErrorResponse.ABORT;
progressDialog.cancel();
break;
}
});
} catch (Exception ex) {
result[0] = FileOperations.ErrorResponse.ABORT;
} }
SwingUtilities.invokeLater(() -> { return result[0];
}
};
if (action == ClipboardService.ClipboardAction.CUT) {
FileOperations.move(itemsToPaste, targetDir, callback);
} else {
FileOperations.copy(itemsToPaste, targetDir, callback);
}
SwingUtilities.invokeLater(() -> {
progressDialog.dispose(); progressDialog.dispose();
loadDirectory(targetDir, false); loadDirectory(targetDir, false);
if (!itemsToPaste.isEmpty()) { if (!itemsToPaste.isEmpty()) {

View File

@ -1267,15 +1267,15 @@ public class MainWindow extends JFrame {
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel; FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory(); File targetDir = targetPanel.getCurrentDirectory();
int result = JOptionPane.showConfirmDialog(this, int result = showConfirmWithBackground(
String.format("Copy %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()), String.format("Copy %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
"Copy", "Copy");
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) { if (result == 0 || result == 1) {
boolean modal = (result == 0);
performFileOperation((callback) -> { performFileOperation((callback) -> {
FileOperations.copy(selectedItems, targetDir, callback); FileOperations.copy(selectedItems, targetDir, callback);
}, "Copy completed", true, targetPanel); }, "Copy completed", true, modal, targetPanel);
} else { } else {
if (activePanel != null && activePanel.getFileTable() != null) { if (activePanel != null && activePanel.getFileTable() != null) {
activePanel.getFileTable().requestFocusInWindow(); activePanel.getFileTable().requestFocusInWindow();
@ -1300,21 +1300,43 @@ public class MainWindow extends JFrame {
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel; FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory(); File targetDir = targetPanel.getCurrentDirectory();
int result = JOptionPane.showConfirmDialog(this, int result = showConfirmWithBackground(
String.format("Move %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()), String.format("Move %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
"Move", "Move");
JOptionPane.OK_CANCEL_OPTION);
if (result == JOptionPane.OK_OPTION) { if (result == 0 || result == 1) {
boolean modal = (result == 0);
performFileOperation((callback) -> { performFileOperation((callback) -> {
FileOperations.move(selectedItems, targetDir, callback); FileOperations.move(selectedItems, targetDir, callback);
}, "Move completed", false, activePanel, targetPanel); }, "Move completed", false, modal, activePanel, targetPanel);
} else { } else {
if (activePanel != null && activePanel.getFileTable() != null) { if (activePanel != null && activePanel.getFileTable() != null) {
activePanel.getFileTable().requestFocusInWindow(); activePanel.getFileTable().requestFocusInWindow();
} }
} }
} }
private int showConfirmWithBackground(String message, String title) {
Object[] options = {"OK", "Background (F2)", "Cancel"};
JOptionPane pane = new JOptionPane(message, JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION, null, options, options[0]);
JDialog dialog = pane.createDialog(this, title);
pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0), "background");
pane.getActionMap().put("background", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
pane.setValue(options[1]);
dialog.dispose();
}
});
dialog.setVisible(true);
Object selectedValue = pane.getValue();
if (selectedValue == null) return 2; // Cancel
if (selectedValue.equals(options[0])) return 0; // OK
if (selectedValue.equals(options[1])) return 1; // Background
return 2; // Cancel
}
/** /**
* Delete selected files * Delete selected files
@ -1960,14 +1982,25 @@ public class MainWindow extends JFrame {
* Execute file operation with error handling * Execute file operation with error handling
*/ */
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, FilePanel... panelsToRefresh) { private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, FilePanel... panelsToRefresh) {
performFileOperation(operation, successMessage, showBytes, null, panelsToRefresh); performFileOperation(operation, successMessage, showBytes, true, null, panelsToRefresh);
}
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, boolean modal, FilePanel... panelsToRefresh) {
performFileOperation(operation, successMessage, showBytes, modal, null, panelsToRefresh);
} }
/** /**
* Execute file operation with error handling and a task to run after completion and refresh. * Execute file operation with error handling and a task to run after completion and refresh.
*/ */
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, Runnable postTask, FilePanel... panelsToRefresh) { private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, Runnable postTask, FilePanel... panelsToRefresh) {
ProgressDialog progressDialog = new ProgressDialog(this, "File Operation"); performFileOperation(operation, successMessage, showBytes, true, postTask, panelsToRefresh);
}
/**
* Execute file operation with error handling and a task to run after completion and refresh.
*/
private void performFileOperation(FileOperation operation, String successMessage, boolean showBytes, boolean modal, Runnable postTask, FilePanel... panelsToRefresh) {
ProgressDialog progressDialog = new ProgressDialog(this, "File Operation", modal);
progressDialog.setDisplayAsBytes(showBytes); progressDialog.setDisplayAsBytes(showBytes);
FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() { FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() {
@ -2012,6 +2045,36 @@ public class MainWindow extends JFrame {
} }
return result[0]; return result[0];
} }
@Override
public FileOperations.ErrorResponse onError(File file, Exception e) {
final FileOperations.ErrorResponse[] result = new FileOperations.ErrorResponse[1];
try {
SwingUtilities.invokeAndWait(() -> {
Object[] options = {"Skip", "Retry", "Abort"};
int n = JOptionPane.showOptionDialog(progressDialog,
"Error operating on file: " + file.getName() + "\n" + e.getMessage(),
"Error",
JOptionPane.DEFAULT_OPTION,
JOptionPane.ERROR_MESSAGE,
null,
options,
options[0]);
switch (n) {
case 0: result[0] = FileOperations.ErrorResponse.SKIP; break;
case 1: result[0] = FileOperations.ErrorResponse.RETRY; break;
default:
result[0] = FileOperations.ErrorResponse.ABORT;
progressDialog.cancel();
break;
}
});
} catch (Exception ex) {
result[0] = FileOperations.ErrorResponse.ABORT;
}
return result[0];
}
}; };
// Run operation in a background thread // Run operation in a background thread

View File

@ -17,7 +17,11 @@ public class ProgressDialog extends JDialog {
private long startTime = -1; private long startTime = -1;
public ProgressDialog(Frame owner, String title) { public ProgressDialog(Frame owner, String title) {
super(owner, title, true); this(owner, title, true);
}
public ProgressDialog(Frame owner, String title, boolean modal) {
super(owner, title, modal);
setLayout(new BorderLayout(10, 10)); setLayout(new BorderLayout(10, 10));
((JPanel)getContentPane()).setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); ((JPanel)getContentPane()).setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));