diff --git a/.gitignore b/.gitignore index eb54cae..f7c6f60 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ buildNumber.properties # OS .DS_Store Thumbs.db +.claude \ No newline at end of file diff --git a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java index c28151a..6b52a26 100644 --- a/src/main/java/cz/kamma/kfmanager/service/FileOperations.java +++ b/src/main/java/cz/kamma/kfmanager/service/FileOperations.java @@ -59,6 +59,7 @@ public class FileOperations { long totalItems = calculateTotalItems(cleanedItems); final long[] currentItem = {0}; final OverwriteResponse[] globalResponse = {null}; + final ErrorResponse[] globalErrorResponse = {null}; for (FileItem item : cleanedItems) { if (callback != null && callback.isCancelled()) break; @@ -95,9 +96,10 @@ public class FileOperations { } while (true) { + if (globalErrorResponse[0] == ErrorResponse.SKIP_ALL) break; try { if (source.isDirectory()) { - copyDirectory(source, target, totalItems, currentItem, callback, globalResponse); + copyDirectory(source, target, totalItems, currentItem, callback, globalResponse, globalErrorResponse); } else { copyFileWithProgress(source, target, totalItems, currentItem, callback); } @@ -107,6 +109,7 @@ public class FileOperations { ErrorResponse res = callback.onError(source, e); if (res == ErrorResponse.ABORT) throw e; if (res == ErrorResponse.RETRY) continue; + if (res == ErrorResponse.SKIP_ALL) globalErrorResponse[0] = ErrorResponse.SKIP_ALL; break; } else { throw e; @@ -163,7 +166,7 @@ public class FileOperations { copyPermissions(source, target); } - private static void copyDirectory(File source, File target, long totalItems, final long[] currentItem, ProgressCallback callback, final OverwriteResponse[] globalResponse) throws IOException { + private static void copyDirectory(File source, File target, long totalItems, final long[] currentItem, ProgressCallback callback, final OverwriteResponse[] globalResponse, final ErrorResponse[] globalErrorResponse) throws IOException { if (!target.exists()) { if (!target.mkdirs()) { throw new IOException("Failed to create directory: " + target.getAbsolutePath()); @@ -179,32 +182,52 @@ public class FileOperations { if (files != null) { for (File file : files) { if (callback != null && callback.isCancelled()) break; + if (globalErrorResponse[0] == ErrorResponse.SKIP_ALL) break; File targetFile = new File(target, file.getName()); - - if (file.isDirectory()) { - copyDirectory(file, targetFile, totalItems, currentItem, callback, globalResponse); - } else { - if (targetFile.exists()) { - if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) { - currentItem[0] += countItems(file); - continue; + + while (true) { + try { + if (file.isDirectory()) { + copyDirectory(file, targetFile, totalItems, currentItem, callback, globalResponse, globalErrorResponse); + } else { + if (targetFile.exists()) { + if (globalResponse[0] == OverwriteResponse.NO_TO_ALL) { + currentItem[0] += countItems(file); + break; + } + if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) { + OverwriteResponse res = callback.confirmOverwrite(file, targetFile); + if (res == OverwriteResponse.CANCEL) return; + if (res == OverwriteResponse.NO_TO_ALL) { + globalResponse[0] = OverwriteResponse.NO_TO_ALL; + currentItem[0] += countItems(file); + break; + } + if (res == OverwriteResponse.NO) { + currentItem[0] += countItems(file); + break; + } + if (res == OverwriteResponse.YES_TO_ALL) + globalResponse[0] = OverwriteResponse.YES_TO_ALL; + } + } + copyFileWithProgress(file, targetFile, totalItems, currentItem, callback); } - if (globalResponse[0] != OverwriteResponse.YES_TO_ALL) { - OverwriteResponse res = callback.confirmOverwrite(file, targetFile); - if (res == OverwriteResponse.CANCEL) return; - if (res == OverwriteResponse.NO_TO_ALL) { - globalResponse[0] = OverwriteResponse.NO_TO_ALL; - currentItem[0] += countItems(file); - continue; - } - if (res == OverwriteResponse.NO) { - currentItem[0] += countItems(file); - continue; - } - if (res == OverwriteResponse.YES_TO_ALL) globalResponse[0] = OverwriteResponse.YES_TO_ALL; + break; + } catch (IOException e) { + if (callback != null) { + ErrorResponse res = callback.onError(file, e); + if (res == ErrorResponse.ABORT) throw e; + if (res == ErrorResponse.RETRY) continue; + if (res == ErrorResponse.SKIP_ALL) globalErrorResponse[0] = ErrorResponse.SKIP_ALL; + // SKIP: Break while(true) to continue with next file in for loop + // we don't increment currentItem here because it's hard to know + // what exactly was processed inside copyDirectory if it failed midway. + break; + } else { + throw e; } } - copyFileWithProgress(file, targetFile, totalItems, currentItem, callback); } } } @@ -273,6 +296,7 @@ public class FileOperations { long totalItems = calculateTotalItems(cleanedItems); long[] currentItem = {0}; final OverwriteResponse[] globalResponse = {null}; + final ErrorResponse[] globalErrorResponse = {null}; for (FileItem item : cleanedItems) { if (callback != null && callback.isCancelled()) break; @@ -306,6 +330,7 @@ public class FileOperations { long itemCount = countItems(source); while (true) { + if (globalErrorResponse[0] == ErrorResponse.SKIP_ALL) break; try { if (source.renameTo(target)) { currentItem[0] += itemCount; @@ -315,7 +340,7 @@ public class FileOperations { } else { // Fallback for cross-device moves if (source.isDirectory()) { - copyDirectory(source, target, totalItems, currentItem, callback, globalResponse); + copyDirectory(source, target, totalItems, currentItem, callback, globalResponse, globalErrorResponse); deleteDirectoryInternal(source); } else { copyFileWithProgress(source, target, totalItems, currentItem, callback); @@ -328,6 +353,7 @@ public class FileOperations { ErrorResponse res = callback.onError(source, e); if (res == ErrorResponse.ABORT) throw e; if (res == ErrorResponse.RETRY) continue; + if (res == ErrorResponse.SKIP_ALL) globalErrorResponse[0] = ErrorResponse.SKIP_ALL; break; } else { throw e; @@ -348,15 +374,17 @@ public class FileOperations { List cleanedItems = cleanDuplicateItems(items); long totalItems = calculateTotalItems(cleanedItems); long[] currentItem = {0}; + final ErrorResponse[] globalErrorResponse = {null}; for (FileItem item : cleanedItems) { if (callback != null && callback.isCancelled()) break; File file = item.getFile(); while (true) { + if (globalErrorResponse[0] == ErrorResponse.SKIP_ALL) break; try { if (file.isDirectory()) { - deleteDirectoryRecursive(file, totalItems, currentItem, callback); + deleteDirectoryRecursive(file, totalItems, currentItem, callback, globalErrorResponse); } else { currentItem[0]++; if (callback != null) { @@ -370,6 +398,7 @@ public class FileOperations { ErrorResponse res = callback.onError(file, e); if (res == ErrorResponse.ABORT) throw e; if (res == ErrorResponse.RETRY) continue; + if (res == ErrorResponse.SKIP_ALL) globalErrorResponse[0] = ErrorResponse.SKIP_ALL; break; } else { throw e; @@ -379,18 +408,36 @@ public class FileOperations { } } - private static void deleteDirectoryRecursive(File directory, long totalItems, long[] currentItem, ProgressCallback callback) throws IOException { + private static void deleteDirectoryRecursive(File directory, long totalItems, long[] currentItem, ProgressCallback callback, final ErrorResponse[] globalErrorResponse) throws IOException { File[] children = directory.listFiles(); if (children != null) { for (File child : children) { - if (child.isDirectory()) { - deleteDirectoryRecursive(child, totalItems, currentItem, callback); - } else { - currentItem[0]++; - if (callback != null) { - callback.onProgress(currentItem[0], totalItems, child.getName()); + if (callback != null && callback.isCancelled()) break; + if (globalErrorResponse[0] == ErrorResponse.SKIP_ALL) break; + while (true) { + try { + if (child.isDirectory()) { + deleteDirectoryRecursive(child, totalItems, currentItem, callback, globalErrorResponse); + } else { + currentItem[0]++; + if (callback != null) { + callback.onProgress(currentItem[0], totalItems, child.getName()); + } + deleteFileWithForce(child); + } + break; + } catch (IOException e) { + if (callback != null) { + ErrorResponse res = callback.onError(child, e); + if (res == ErrorResponse.ABORT) throw e; + if (res == ErrorResponse.RETRY) continue; + if (res == ErrorResponse.SKIP_ALL) globalErrorResponse[0] = ErrorResponse.SKIP_ALL; + // SKIP: + break; + } else { + throw e; + } } - deleteFileWithForce(child); } } } @@ -490,9 +537,23 @@ public class FileOperations { public static void deleteFromFtp(List items, ProgressCallback callback) throws IOException { for (FileItem item : items) { if (callback != null && callback.isCancelled()) break; - FtpService.deleteOnFtp(item.getFtpProfile(), item.getFtpPath()); - if (callback != null) { - callback.onFileProgress(item.getSize(), item.getSize()); + while (true) { + try { + FtpService.deleteOnFtp(item.getFtpProfile(), item.getFtpPath()); + if (callback != null) { + callback.onFileProgress(item.getSize(), item.getSize()); + } + break; + } catch (IOException e) { + if (callback != null) { + ErrorResponse res = callback.onError(item.getFile(), e); + if (res == ErrorResponse.ABORT) throw e; + if (res == ErrorResponse.RETRY) continue; + break; + } else { + throw e; + } + } } } } @@ -504,23 +565,37 @@ public class FileOperations { for (FileItem item : items) { if (callback != null && callback.isCancelled()) break; - if (item.isDirectory()) { - if (item.isFtp()) { - File tempDir = java.nio.file.Files.createTempDirectory("kf-ftp-copy").toFile(); - copyFromFtp(java.util.Collections.singletonList(item), tempDir, callback); - uploadLocalToFtp(profile, tempDir, remotePath + "/" + item.getName(), callback); - deleteRecursively(tempDir); - } else { - uploadLocalToFtp(profile, item.getFile(), remotePath + "/" + item.getName(), callback); - } - } else { - if (item.isFtp()) { - File tempFile = java.nio.file.Files.createTempFile("kf-ftp-copy", ".tmp").toFile(); - FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), tempFile, callback); - FtpService.uploadFile(profile, tempFile, remotePath + "/" + item.getName(), callback); - tempFile.delete(); - } else { - FtpService.uploadFile(profile, item.getFile(), remotePath + "/" + item.getName(), callback); + while (true) { + try { + if (item.isDirectory()) { + if (item.isFtp()) { + File tempDir = java.nio.file.Files.createTempDirectory("kf-ftp-copy").toFile(); + copyFromFtp(java.util.Collections.singletonList(item), tempDir, callback); + uploadLocalToFtp(profile, tempDir, remotePath + "/" + item.getName(), callback); + deleteRecursively(tempDir); + } else { + uploadLocalToFtp(profile, item.getFile(), remotePath + "/" + item.getName(), callback); + } + } else { + if (item.isFtp()) { + File tempFile = java.nio.file.Files.createTempFile("kf-ftp-copy", ".tmp").toFile(); + FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), tempFile, callback); + FtpService.uploadFile(profile, tempFile, remotePath + "/" + item.getName(), callback); + tempFile.delete(); + } else { + FtpService.uploadFile(profile, item.getFile(), remotePath + "/" + item.getName(), callback); + } + } + break; + } catch (IOException e) { + if (callback != null) { + ErrorResponse res = callback.onError(item.getFile(), e); + if (res == ErrorResponse.ABORT) throw e; + if (res == ErrorResponse.RETRY) continue; + break; + } else { + throw e; + } } } } @@ -543,22 +618,36 @@ public class FileOperations { for (FileItem item : ftpItems) { if (callback != null && callback.isCancelled()) break; - - if (callback != null) { - callback.onProgress(currentItem[0], totalItems, item.getName()); - } - if (item.isDirectory()) { - File targetDir = new File(localTarget, item.getName()); - FtpService.downloadDirectory(item.getFtpProfile(), item.getFtpPath(), targetDir, callback); - } else { - File targetFile = new File(localTarget, item.getName()); - FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), targetFile, callback); - } - - currentItem[0]++; - if (callback != null) { - callback.onProgress(currentItem[0], totalItems, item.getName()); + while (true) { + try { + if (callback != null) { + callback.onProgress(currentItem[0], totalItems, item.getName()); + } + + if (item.isDirectory()) { + File targetDir = new File(localTarget, item.getName()); + FtpService.downloadDirectory(item.getFtpProfile(), item.getFtpPath(), targetDir, callback); + } else { + File targetFile = new File(localTarget, item.getName()); + FtpService.downloadFile(item.getFtpProfile(), item.getFtpPath(), targetFile, callback); + } + + currentItem[0]++; + if (callback != null) { + callback.onProgress(currentItem[0], totalItems, item.getName()); + } + break; + } catch (IOException e) { + if (callback != null) { + ErrorResponse res = callback.onError(item.getFile(), e); + if (res == ErrorResponse.ABORT) throw e; + if (res == ErrorResponse.RETRY) continue; + break; + } else { + throw e; + } + } } } } @@ -1635,7 +1724,7 @@ public class FileOperations { } public enum ErrorResponse { - RETRY, SKIP, ABORT + RETRY, SKIP, SKIP_ALL, ABORT } public enum SymlinkResponse { diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java index 05af5c5..73b1ba3 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java +++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java @@ -3767,7 +3767,7 @@ public class MainWindow extends JFrame { final FileOperations.ErrorResponse[] result = new FileOperations.ErrorResponse[1]; try { SwingUtilities.invokeAndWait(() -> { - Object[] options = {"Skip", "Retry", "Abort"}; + Object[] options = {"Skip", "Skip All", "Retry", "Abort"}; int n = JOptionPane.showOptionDialog(progressDialog, "Error operating on file: " + file.getName() + "\n" + e.getMessage(), "Error", @@ -3779,7 +3779,8 @@ public class MainWindow extends JFrame { switch (n) { case 0: result[0] = FileOperations.ErrorResponse.SKIP; break; - case 1: result[0] = FileOperations.ErrorResponse.RETRY; break; + case 1: result[0] = FileOperations.ErrorResponse.SKIP_ALL; break; + case 2: result[0] = FileOperations.ErrorResponse.RETRY; break; default: result[0] = FileOperations.ErrorResponse.ABORT; progressDialog.cancel();