archive operations rewritten

This commit is contained in:
Radek Davidek 2026-05-12 13:05:29 +02:00
parent 0a3f3e75e0
commit c2865750c0
4 changed files with 136 additions and 3 deletions

View File

@ -15,7 +15,7 @@ import java.io.InputStreamReader;
*/ */
public class MainApp { public class MainApp {
public static final String APP_VERSION = "1.4.5"; public static final String APP_VERSION = "1.4.6";
public enum OS { public enum OS {
WINDOWS, LINUX, MACOS, UNKNOWN WINDOWS, LINUX, MACOS, UNKNOWN

View File

@ -9,6 +9,7 @@ import java.awt.event.KeyEvent;
import java.awt.event.InputEvent; import java.awt.event.InputEvent;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -47,6 +48,8 @@ public class FileEditor extends JFrame {
private final int pageSizeBytes = 64 * 1024; // 64 KB page size private final int pageSizeBytes = 64 * 1024; // 64 KB page size
// Allow loading entire file into memory up to this limit (100 MB) // Allow loading entire file into memory up to this limit (100 MB)
private final long maxFullLoadBytes = 100L * 1024L * 1024L; // 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 hexControlPanel = null;
private JPanel northPanel = null; private JPanel northPanel = null;
private JButton prevPageBtn = null; private JButton prevPageBtn = null;
@ -1073,7 +1076,7 @@ public class FileEditor extends JFrame {
SwingUtilities.invokeLater(() -> textArea.requestFocusInWindow()); SwingUtilities.invokeLater(() -> textArea.requestFocusInWindow());
} else { } else {
// Small or text file: load fully // Small or text file: load fully
fileBytes = Files.readAllBytes(file.toPath()); fileBytes = readFileBytesWithChunking(file, size);
boolean binary = isBinary(fileBytes); boolean binary = isBinary(fileBytes);
if (binary && readOnly) { if (binary && readOnly) {
hexMode = true; hexMode = true;
@ -1115,6 +1118,22 @@ public class FileEditor extends JFrame {
return nonPrintable > (len / 4); 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) { private boolean isImageFile(File f) {
String name = f.getName().toLowerCase(); String name = f.getName().toLowerCase();
return name.endsWith(".jpg") || name.endsWith(".jpeg") || return name.endsWith(".jpg") || name.endsWith(".jpeg") ||

View File

@ -87,6 +87,7 @@ public class FilePanelTab extends JPanel {
private Path currentArchiveTempDir = null; private Path currentArchiveTempDir = null;
private File currentArchiveSourceFile = null; private File currentArchiveSourceFile = null;
private String currentArchivePassword = null; private String currentArchivePassword = null;
private long archiveExtractTime = 0; // Track time of extraction to detect changes
private File archiveReturnDirectory = null; private File archiveReturnDirectory = null;
private Point archiveReturnViewPosition = null; private Point archiveReturnViewPosition = null;
private boolean inlineRenameActive = false; private boolean inlineRenameActive = false;
@ -1923,12 +1924,26 @@ public class FilePanelTab extends JPanel {
/** /**
* Cleanup previous archive temp dir when navigating away from it. * 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) { private void cleanupArchiveTempDirIfNeeded(File newDirectory) {
try { try {
if (currentArchiveTempDir != null) { if (currentArchiveTempDir != null) {
Path newPath = (newDirectory != null) ? newDirectory.toPath().toAbsolutePath().normalize() : null; Path newPath = (newDirectory != null) ? newDirectory.toPath().toAbsolutePath().normalize() : null;
if (newPath == null || !newPath.startsWith(currentArchiveTempDir)) { 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); deleteTempDirRecursively(currentArchiveTempDir);
clearOpenedArchiveSession(); clearOpenedArchiveSession();
} }
@ -1947,11 +1962,22 @@ public class FilePanelTab extends JPanel {
temp -> { temp -> {
try { try {
if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) { 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); deleteTempDirRecursively(currentArchiveTempDir);
} }
} catch (Exception ignore) {} } catch (Exception ignore) {}
currentArchiveTempDir = temp; currentArchiveTempDir = temp;
currentArchiveSourceFile = archive; currentArchiveSourceFile = archive;
archiveExtractTime = System.currentTimeMillis(); // Track extraction time
if (finalEntryName == null || finalEntryName.isBlank()) { if (finalEntryName == null || finalEntryName.isBlank()) {
loadDirectory(temp.toFile(), true, true); loadDirectory(temp.toFile(), true, true);
@ -2046,11 +2072,22 @@ public class FilePanelTab extends JPanel {
temp -> { temp -> {
try { try {
if (currentArchiveTempDir != null && !currentArchiveTempDir.equals(temp)) { 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); deleteTempDirRecursively(currentArchiveTempDir);
} }
} catch (Exception ignore) {} } catch (Exception ignore) {}
currentArchiveTempDir = temp; currentArchiveTempDir = temp;
currentArchiveSourceFile = archiveFile; currentArchiveSourceFile = archiveFile;
archiveExtractTime = System.currentTimeMillis(); // Track extraction time
loadDirectory(temp.toFile()); loadDirectory(temp.toFile());
}, },
error -> { error -> {
@ -2587,6 +2624,7 @@ public class FilePanelTab extends JPanel {
} catch (Exception ignore) {} } catch (Exception ignore) {}
currentArchiveTempDir = temp; currentArchiveTempDir = temp;
currentArchiveSourceFile = archiveFile; currentArchiveSourceFile = archiveFile;
archiveExtractTime = System.currentTimeMillis(); // Track extraction time
loadDirectory(temp.toFile(), true, true); loadDirectory(temp.toFile(), true, true);
}, },
error -> { error -> {
@ -3001,6 +3039,18 @@ public class FilePanelTab extends JPanel {
File parent = currentArchiveSourceFile.getParentFile(); File parent = currentArchiveSourceFile.getParentFile();
if (parent != null) { if (parent != null) {
String archiveName = currentArchiveSourceFile.getName(); 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 // cleanup temp dir before switching back
deleteTempDirRecursively(currentArchiveTempDir); deleteTempDirRecursively(currentArchiveTempDir);
clearOpenedArchiveSession(); clearOpenedArchiveSession();
@ -3120,6 +3170,51 @@ public class FilePanelTab extends JPanel {
currentArchiveTempDir = null; currentArchiveTempDir = null;
currentArchiveSourceFile = null; currentArchiveSourceFile = null;
currentArchivePassword = 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. * Cleanup resources when tab is closed.
*/ */
public void cleanup() { 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) { if (isFtpTab && ftpProfile != null) {
FtpService.disconnect(ftpProfile); FtpService.disconnect(ftpProfile);
} }

View File

@ -2873,7 +2873,7 @@ public class MainWindow extends JFrame {
if (forceInternalEditor) { if (forceInternalEditor) {
FileEditor editor = new FileEditor(this, file, config, false); 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() { editor.addWindowListener(new java.awt.event.WindowAdapter() {
@Override @Override
public void windowClosed(java.awt.event.WindowEvent e) { public void windowClosed(java.awt.event.WindowEvent e) {