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 static final String APP_VERSION = "0.0.4";
public static final String APP_VERSION = "0.0.5";
public static void main(String[] args) {
// Set application name for X11/Wayland WM_CLASS

View File

@ -49,10 +49,24 @@ public class FileOperations {
}
}
if (source.isDirectory()) {
copyDirectory(source.toPath(), target.toPath(), totalSize, currentCopied, callback, globalResponse);
} else {
copyFileWithProgress(source.toPath(), target.toPath(), totalSize, currentCopied, callback);
while (true) {
try {
if (source.isDirectory()) {
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 {
long initialTotalCopied = totalCopied[0];
try (InputStream in = Files.newInputStream(source);
OutputStream out = Files.newOutputStream(target)) {
byte[] buffer = new byte[8192];
@ -96,6 +111,9 @@ public class FileOperations {
callback.onProgress(totalCopied[0], totalSize, source.getFileName().toString());
}
}
} catch (IOException e) {
totalCopied[0] = initialTotalCopied;
throw e;
}
Files.setLastModifiedTime(target, Files.getLastModifiedTime(source));
}
@ -105,8 +123,20 @@ public class FileOperations {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = target.resolve(source.relativize(dir));
Files.createDirectories(targetDir);
return FileVisitResult.CONTINUE;
while (true) {
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
@ -128,8 +158,20 @@ public class FileOperations {
}
}
copyFileWithProgress(file, targetFile, totalSize, totalCopied, callback);
return FileVisitResult.CONTINUE;
while (true) {
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());
}
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());
}
if (file.isDirectory()) {
deleteDirectory(file.toPath());
} else {
Files.delete(file.toPath());
while (true) {
try {
if (file.isDirectory()) {
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
*/
private static void deleteDirectory(Path directory) throws IOException {
private static void deleteDirectory(Path directory, ProgressCallback callback) throws IOException {
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
while (true) {
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
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
if (exc != null) throw exc;
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
}
public enum ErrorResponse {
SKIP, RETRY, ABORT
}
public interface ProgressCallback {
void onProgress(long current, long total, String currentFile);
default boolean isCancelled() { return false; }
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() {
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(() -> {
progressDialog.dispose();
@ -1781,14 +1810,44 @@ public class FilePanelTab extends JPanel {
}
return result[0];
}
};
if (action == ClipboardService.ClipboardAction.CUT) {
FileOperations.move(itemsToPaste, targetDir, callback);
} else {
FileOperations.copy(itemsToPaste, targetDir, callback);
@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;
}
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();
loadDirectory(targetDir, false);
if (!itemsToPaste.isEmpty()) {

View File

@ -1267,15 +1267,15 @@ public class MainWindow extends JFrame {
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory();
int result = JOptionPane.showConfirmDialog(this,
int result = showConfirmWithBackground(
String.format("Copy %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
"Copy",
JOptionPane.OK_CANCEL_OPTION);
"Copy");
if (result == JOptionPane.OK_OPTION) {
if (result == 0 || result == 1) {
boolean modal = (result == 0);
performFileOperation((callback) -> {
FileOperations.copy(selectedItems, targetDir, callback);
}, "Copy completed", true, targetPanel);
}, "Copy completed", true, modal, targetPanel);
} else {
if (activePanel != null && activePanel.getFileTable() != null) {
activePanel.getFileTable().requestFocusInWindow();
@ -1300,15 +1300,15 @@ public class MainWindow extends JFrame {
FilePanel targetPanel = (activePanel == leftPanel) ? rightPanel : leftPanel;
File targetDir = targetPanel.getCurrentDirectory();
int result = JOptionPane.showConfirmDialog(this,
int result = showConfirmWithBackground(
String.format("Move %d items to:\n%s", selectedItems.size(), targetDir.getAbsolutePath()),
"Move",
JOptionPane.OK_CANCEL_OPTION);
"Move");
if (result == JOptionPane.OK_OPTION) {
if (result == 0 || result == 1) {
boolean modal = (result == 0);
performFileOperation((callback) -> {
FileOperations.move(selectedItems, targetDir, callback);
}, "Move completed", false, activePanel, targetPanel);
}, "Move completed", false, modal, activePanel, targetPanel);
} else {
if (activePanel != null && activePanel.getFileTable() != null) {
activePanel.getFileTable().requestFocusInWindow();
@ -1316,6 +1316,28 @@ public class MainWindow extends JFrame {
}
}
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
*/
@ -1960,14 +1982,25 @@ public class MainWindow extends JFrame {
* Execute file operation with error handling
*/
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.
*/
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);
FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() {
@ -2012,6 +2045,36 @@ public class MainWindow extends JFrame {
}
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

View File

@ -17,7 +17,11 @@ public class ProgressDialog extends JDialog {
private long startTime = -1;
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));
((JPanel)getContentPane()).setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));