From c2865750c0c384f93bcc7ccc51428c4da9a4e115 Mon Sep 17 00:00:00 2001 From: Radek Davidek Date: Tue, 12 May 2026 13:05:29 +0200 Subject: [PATCH] archive operations rewritten --- src/main/java/cz/kamma/kfmanager/MainApp.java | 2 +- .../cz/kamma/kfmanager/ui/FileEditor.java | 21 +++- .../cz/kamma/kfmanager/ui/FilePanelTab.java | 114 ++++++++++++++++++ .../cz/kamma/kfmanager/ui/MainWindow.java | 2 +- 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/src/main/java/cz/kamma/kfmanager/MainApp.java b/src/main/java/cz/kamma/kfmanager/MainApp.java index 3d1070b..3decf5f 100644 --- a/src/main/java/cz/kamma/kfmanager/MainApp.java +++ b/src/main/java/cz/kamma/kfmanager/MainApp.java @@ -15,7 +15,7 @@ import java.io.InputStreamReader; */ public class MainApp { - public static final String APP_VERSION = "1.4.5"; + public static final String APP_VERSION = "1.4.6"; public enum OS { WINDOWS, LINUX, MACOS, UNKNOWN diff --git a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java index 06909d0..cd0685f 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FileEditor.java @@ -9,6 +9,7 @@ import java.awt.event.KeyEvent; import java.awt.event.InputEvent; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.file.Files; import java.util.regex.Pattern; @@ -47,6 +48,8 @@ public class FileEditor extends JFrame { private final int pageSizeBytes = 64 * 1024; // 64 KB page size // Allow loading entire file into memory up to this limit (100 MB) private final long maxFullLoadBytes = 100L * 1024L * 1024L; // 100 MB + private static final int CHUNK_SIZE_BYTES = 2 * 1024 * 1024; // 2 MB + private static final long CHUNK_THRESHOLD_BYTES = 2L * 1024L * 1024L; // 2 MB private JPanel hexControlPanel = null; private JPanel northPanel = null; private JButton prevPageBtn = null; @@ -1073,7 +1076,7 @@ public class FileEditor extends JFrame { SwingUtilities.invokeLater(() -> textArea.requestFocusInWindow()); } else { // Small or text file: load fully - fileBytes = Files.readAllBytes(file.toPath()); + fileBytes = readFileBytesWithChunking(file, size); boolean binary = isBinary(fileBytes); if (binary && readOnly) { hexMode = true; @@ -1115,6 +1118,22 @@ public class FileEditor extends JFrame { return nonPrintable > (len / 4); } + private byte[] readFileBytesWithChunking(File source, long knownSize) throws IOException { + if (knownSize <= CHUNK_THRESHOLD_BYTES) { + return Files.readAllBytes(source.toPath()); + } + + try (InputStream in = Files.newInputStream(source.toPath()); + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream((int) Math.min(Integer.MAX_VALUE, knownSize))) { + byte[] buffer = new byte[CHUNK_SIZE_BYTES]; + int read; + while ((read = in.read(buffer)) > 0) { + out.write(buffer, 0, read); + } + return out.toByteArray(); + } + } + private boolean isImageFile(File f) { String name = f.getName().toLowerCase(); return name.endsWith(".jpg") || name.endsWith(".jpeg") || diff --git a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java index c78a4ba..b9db6b5 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/cz/kamma/kfmanager/ui/FilePanelTab.java @@ -87,6 +87,7 @@ public class FilePanelTab extends JPanel { private Path currentArchiveTempDir = null; private File currentArchiveSourceFile = null; private String currentArchivePassword = null; + private long archiveExtractTime = 0; // Track time of extraction to detect changes private File archiveReturnDirectory = null; private Point archiveReturnViewPosition = null; private boolean inlineRenameActive = false; @@ -1923,12 +1924,26 @@ public class FilePanelTab extends JPanel { /** * Cleanup previous archive temp dir when navigating away from it. + * If changes were made to the archive, save them back to the original file. */ private void cleanupArchiveTempDirIfNeeded(File newDirectory) { try { if (currentArchiveTempDir != null) { Path newPath = (newDirectory != null) ? newDirectory.toPath().toAbsolutePath().normalize() : null; if (newPath == null || !newPath.startsWith(currentArchiveTempDir)) { + // Before deleting temp dir, check if archive was modified and sync if needed + if (currentArchiveSourceFile != null && FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) { + if (hasArchiveBeenModified()) { + try { + // Synchronously rewrite the archive with all changes made to the temp directory + FileOperations.rewriteArchiveFromDirectory(currentArchiveTempDir.toFile(), currentArchiveSourceFile, currentArchivePassword, null); + } catch (IOException e) { + System.err.println("Warning: Failed to save changes to archive " + currentArchiveSourceFile.getName() + ": " + e.getMessage()); + // Continue with cleanup even if sync fails + } + } + } + deleteTempDirRecursively(currentArchiveTempDir); clearOpenedArchiveSession(); } @@ -1947,11 +1962,22 @@ public class FilePanelTab extends JPanel { temp -> { try { if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) { + // Save changes from previous archive before switching (only if modified) + if (currentArchiveSourceFile != null && FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) { + if (hasArchiveBeenModified()) { + try { + FileOperations.rewriteArchiveFromDirectory(currentArchiveTempDir.toFile(), currentArchiveSourceFile, currentArchivePassword, null); + } catch (IOException e) { + System.err.println("Warning: Failed to save changes to archive " + currentArchiveSourceFile.getName() + ": " + e.getMessage()); + } + } + } deleteTempDirRecursively(currentArchiveTempDir); } } catch (Exception ignore) {} currentArchiveTempDir = temp; currentArchiveSourceFile = archive; + archiveExtractTime = System.currentTimeMillis(); // Track extraction time if (finalEntryName == null || finalEntryName.isBlank()) { loadDirectory(temp.toFile(), true, true); @@ -2046,11 +2072,22 @@ public class FilePanelTab extends JPanel { temp -> { try { if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) { + // Save changes from previous archive before switching (only if modified) + if (currentArchiveSourceFile != null && FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) { + if (hasArchiveBeenModified()) { + try { + FileOperations.rewriteArchiveFromDirectory(currentArchiveTempDir.toFile(), currentArchiveSourceFile, currentArchivePassword, null); + } catch (IOException e) { + System.err.println("Warning: Failed to save changes to archive " + currentArchiveSourceFile.getName() + ": " + e.getMessage()); + } + } + } deleteTempDirRecursively(currentArchiveTempDir); } } catch (Exception ignore) {} currentArchiveTempDir = temp; currentArchiveSourceFile = archiveFile; + archiveExtractTime = System.currentTimeMillis(); // Track extraction time loadDirectory(temp.toFile()); }, error -> { @@ -2587,6 +2624,7 @@ public class FilePanelTab extends JPanel { } catch (Exception ignore) {} currentArchiveTempDir = temp; currentArchiveSourceFile = archiveFile; + archiveExtractTime = System.currentTimeMillis(); // Track extraction time loadDirectory(temp.toFile(), true, true); }, error -> { @@ -3001,6 +3039,18 @@ public class FilePanelTab extends JPanel { File parent = currentArchiveSourceFile.getParentFile(); if (parent != null) { String archiveName = currentArchiveSourceFile.getName(); + + // Save any changes made to the archive before leaving (only if modified) + if (FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) { + if (hasArchiveBeenModified()) { + try { + FileOperations.rewriteArchiveFromDirectory(currentArchiveTempDir.toFile(), currentArchiveSourceFile, currentArchivePassword, null); + } catch (IOException e) { + System.err.println("Warning: Failed to save changes to archive " + archiveName + ": " + e.getMessage()); + } + } + } + // cleanup temp dir before switching back deleteTempDirRecursively(currentArchiveTempDir); clearOpenedArchiveSession(); @@ -3120,6 +3170,51 @@ public class FilePanelTab extends JPanel { currentArchiveTempDir = null; currentArchiveSourceFile = null; currentArchivePassword = null; + archiveExtractTime = 0; + } + + /** + * Check if the archive content was modified since extraction. + * Uses recursive directory traversal to detect any changes: + * - Files added/deleted + * - File modifications (size or modification time changes) + * Returns true if changes detected, false otherwise. + */ + private boolean hasArchiveBeenModified() { + if (currentArchiveTempDir == null || archiveExtractTime <= 0) { + return false; // No archive loaded or timestamp not set + } + + File tempDir = currentArchiveTempDir.toFile(); + if (!tempDir.exists()) { + return false; + } + + try { + return directoryHasChanges(tempDir, archiveExtractTime); + } catch (Exception e) { + // On error, assume changes to be safe + System.err.println("Warning: Could not determine if archive was modified: " + e.getMessage()); + return true; + } + } + + /** + * Recursively check if any file/directory in the tree has been modified after given time. + */ + private boolean directoryHasChanges(File dir, long extractTime) { + File[] files = dir.listFiles(); + if (files == null) return false; + + for (File file : files) { + if (file.lastModified() > extractTime) { + return true; // File was modified/created after extraction + } + if (file.isDirectory() && directoryHasChanges(file, extractTime)) { + return true; // Recursively check subdirectories + } + } + return false; } /** @@ -4431,6 +4526,25 @@ public class FilePanelTab extends JPanel { * Cleanup resources when tab is closed. */ public void cleanup() { + // Save any changes made to currently open archive before cleanup (only if modified) + if (currentArchiveTempDir != null && currentArchiveSourceFile != null) { + if (FileOperations.supportsArchiveRewrite(currentArchiveSourceFile)) { + if (hasArchiveBeenModified()) { + try { + FileOperations.rewriteArchiveFromDirectory(currentArchiveTempDir.toFile(), currentArchiveSourceFile, currentArchivePassword, null); + } catch (IOException e) { + System.err.println("Warning: Failed to save changes to archive " + currentArchiveSourceFile.getName() + " on tab close: " + e.getMessage()); + } + } + } + try { + deleteTempDirRecursively(currentArchiveTempDir); + } catch (Exception e) { + System.err.println("Warning: Failed to cleanup archive temp directory: " + e.getMessage()); + } + clearOpenedArchiveSession(); + } + if (isFtpTab && ftpProfile != null) { FtpService.disconnect(ftpProfile); } diff --git a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java index 86aa368..c511596 100644 --- a/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java +++ b/src/main/java/cz/kamma/kfmanager/ui/MainWindow.java @@ -2873,7 +2873,7 @@ public class MainWindow extends JFrame { if (forceInternalEditor) { FileEditor editor = new FileEditor(this, file, config, false); - editor.setOnSaveSuccess(() -> syncArchiveAfterInternalEdit(file)); + // Archive will be synced automatically when user leaves it, not immediately after edit editor.addWindowListener(new java.awt.event.WindowAdapter() { @Override public void windowClosed(java.awt.event.WindowEvent e) {