better archive support

This commit is contained in:
Radek Davidek 2026-03-30 18:27:47 +02:00
parent be40fe132f
commit 4e1b4feac7
5 changed files with 146 additions and 6 deletions

0
.codex Normal file
View File

View File

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

View File

@ -26,6 +26,7 @@ import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import cz.kamma.kfmanager.model.FileItem; import cz.kamma.kfmanager.model.FileItem;
import net.lingala.zip4j.ZipFile; import net.lingala.zip4j.ZipFile;
@ -1093,6 +1094,87 @@ public class FileOperations {
} }
} }
} }
/**
* True when archive can be rewritten from extracted temp directory.
*/
public static boolean supportsArchiveRewrite(File archiveFile) {
if (archiveFile == null) return false;
String n = archiveFile.getName().toLowerCase();
return n.endsWith(".zip") || n.endsWith(".jar") || n.endsWith(".war");
}
/**
* Rebuild zip-like archive from already extracted directory content.
*/
public static void rewriteArchiveFromDirectory(File extractedRootDir, File targetArchiveFile, ProgressCallback callback) throws IOException {
rewriteArchiveFromDirectory(extractedRootDir, targetArchiveFile, null, callback);
}
public static void rewriteArchiveFromDirectory(File extractedRootDir, File targetArchiveFile, String password, ProgressCallback callback) throws IOException {
if (extractedRootDir == null || !extractedRootDir.isDirectory()) {
throw new IOException("Extracted archive directory does not exist");
}
if (targetArchiveFile == null) {
throw new IOException("Target archive is not defined");
}
if (!supportsArchiveRewrite(targetArchiveFile)) {
throw new IOException("Updating this archive format is not supported: " + targetArchiveFile.getName());
}
boolean archiveEncrypted = false;
if (targetArchiveFile.exists()) {
try (ZipFile existing = new ZipFile(targetArchiveFile)) {
archiveEncrypted = existing.isEncrypted();
} catch (IOException e) {
throw e;
} catch (Exception e) {
throw new IOException("Cannot inspect archive: " + e.getMessage(), e);
}
}
if (archiveEncrypted && (password == null || password.isBlank())) {
throw new IOException("Password is required to update encrypted archive");
}
Path tempArchivePath = Files.createTempFile("kfmanager-archive-sync-", ".zip");
File tempArchiveFile = tempArchivePath.toFile();
try {
File[] children = extractedRootDir.listFiles();
if (children == null || children.length == 0) {
if (archiveEncrypted) {
throw new IOException("Updating encrypted archive to empty content is not supported");
}
createEmptyZip(tempArchiveFile);
} else {
List<FileItem> items = new ArrayList<>();
for (File child : children) {
items.add(new FileItem(child));
}
zip(items, tempArchiveFile, archiveEncrypted ? password : null, callback);
}
try {
Files.move(tempArchivePath, targetArchiveFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException moveError) {
throw new IOException("Failed to update archive " + targetArchiveFile.getName() + ": " + moveError.getMessage(), moveError);
}
} finally {
try {
Files.deleteIfExists(tempArchivePath);
} catch (Exception ignore) {
}
}
}
private static void createEmptyZip(File targetZipFile) throws IOException {
if (targetZipFile.exists() && !targetZipFile.delete()) {
throw new IOException("Cannot recreate archive: " + targetZipFile.getAbsolutePath());
}
try (OutputStream os = Files.newOutputStream(targetZipFile.toPath());
ZipOutputStream zos = new ZipOutputStream(os)) {
// intentionally empty; valid empty ZIP archive
}
}
/** /**
* Extract an archive into a target directory * Extract an archive into a target directory

View File

@ -76,6 +76,7 @@ public class FilePanelTab extends JPanel {
// we can cleanup older temp directories when navigation changes. // we can cleanup older temp directories when navigation changes.
private Path currentArchiveTempDir = null; private Path currentArchiveTempDir = null;
private File currentArchiveSourceFile = null; private File currentArchiveSourceFile = null;
private String currentArchivePassword = null;
private File archiveReturnDirectory = null; private File archiveReturnDirectory = null;
private Point archiveReturnViewPosition = null; private Point archiveReturnViewPosition = null;
private boolean inlineRenameActive = false; private boolean inlineRenameActive = false;
@ -1618,7 +1619,7 @@ public class FilePanelTab extends JPanel {
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)) {
deleteTempDirRecursively(currentArchiveTempDir); deleteTempDirRecursively(currentArchiveTempDir);
currentArchiveTempDir = null; clearOpenedArchiveSession();
} }
} }
} catch (Exception ignore) { } catch (Exception ignore) {
@ -1665,6 +1666,7 @@ public class FilePanelTab extends JPanel {
if (archive == null || !archive.isFile()) return null; if (archive == null || !archive.isFile()) return null;
try { try {
Path tempDir = Files.createTempDirectory("kfmanager-archive-"); Path tempDir = Files.createTempDirectory("kfmanager-archive-");
final String[] usedPassword = new String[1];
FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() { FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() {
@Override @Override
public void onProgress(long current, long total, String currentFile) {} public void onProgress(long current, long total, String currentFile) {}
@ -1701,6 +1703,7 @@ public class FilePanelTab extends JPanel {
Object selectedValue = pane.getValue(); Object selectedValue = pane.getValue();
if (selectedValue != null && (Integer) selectedValue == JOptionPane.OK_OPTION) { if (selectedValue != null && (Integer) selectedValue == JOptionPane.OK_OPTION) {
result[0] = new String(pf.getPassword()); result[0] = new String(pf.getPassword());
usedPassword[0] = result[0];
} }
}; };
@ -1715,12 +1718,14 @@ public class FilePanelTab extends JPanel {
return result[0]; return result[0];
} }
}); });
currentArchivePassword = usedPassword[0];
return tempDir; return tempDir;
} catch (Exception ex) { } catch (Exception ex) {
// extraction failed; attempt best-effort cleanup // extraction failed; attempt best-effort cleanup
try { try {
if (currentArchiveTempDir != null) deleteTempDirRecursively(currentArchiveTempDir); if (currentArchiveTempDir != null) deleteTempDirRecursively(currentArchiveTempDir);
} catch (Exception ignore) {} } catch (Exception ignore) {}
currentArchivePassword = null;
return null; return null;
} }
} }
@ -1737,6 +1742,28 @@ public class FilePanelTab extends JPanel {
} catch (IOException ignore) { } catch (IOException ignore) {
} }
} }
public boolean isDirectoryInsideOpenedArchive(File directory) {
if (directory == null || currentArchiveTempDir == null || currentArchiveSourceFile == null) return false;
try {
Path dirPath = directory.toPath().toAbsolutePath().normalize();
Path archiveRoot = currentArchiveTempDir.toAbsolutePath().normalize();
return dirPath.startsWith(archiveRoot);
} catch (Exception ignore) {
return false;
}
}
public boolean canSyncOpenedArchive() {
return currentArchiveTempDir != null
&& currentArchiveSourceFile != null
&& FileOperations.supportsArchiveRewrite(currentArchiveSourceFile);
}
public void syncOpenedArchiveChanges(FileOperations.ProgressCallback callback) throws IOException {
if (currentArchiveTempDir == null || currentArchiveSourceFile == null) return;
FileOperations.rewriteArchiveFromDirectory(currentArchiveTempDir.toFile(), currentArchiveSourceFile, currentArchivePassword, callback);
}
private void openSelectedItem() { private void openSelectedItem() {
int selectedRow = fileTable.getSelectedRow(); int selectedRow = fileTable.getSelectedRow();
@ -2270,8 +2297,7 @@ public class FilePanelTab extends JPanel {
String archiveName = currentArchiveSourceFile.getName(); String archiveName = currentArchiveSourceFile.getName();
// cleanup temp dir before switching back // cleanup temp dir before switching back
deleteTempDirRecursively(currentArchiveTempDir); deleteTempDirRecursively(currentArchiveTempDir);
currentArchiveTempDir = null; clearOpenedArchiveSession();
currentArchiveSourceFile = null;
loadDirectory(parent, false, true, () -> { loadDirectory(parent, false, true, () -> {
// Restore original viewport position and keep focus on archive item. // Restore original viewport position and keep focus on archive item.
// In BRIEF mode some selection listeners may still run later, so apply once more on EDT tail. // In BRIEF mode some selection listeners may still run later, so apply once more on EDT tail.
@ -2384,6 +2410,12 @@ public class FilePanelTab extends JPanel {
archiveReturnViewPosition = null; archiveReturnViewPosition = null;
} }
private void clearOpenedArchiveSession() {
currentArchiveTempDir = null;
currentArchiveSourceFile = null;
currentArchivePassword = null;
}
/** /**
* Public wrapper to select an item by name from outside this class. * Public wrapper to select an item by name from outside this class.
* Useful for other UI components to request focusing a specific file. * Useful for other UI components to request focusing a specific file.
@ -2738,6 +2770,13 @@ public class FilePanelTab extends JPanel {
} else { } else {
FileOperations.copy(itemsToPaste, targetDir, callback); FileOperations.copy(itemsToPaste, targetDir, callback);
} }
if (isDirectoryInsideOpenedArchive(targetDir)) {
if (!canSyncOpenedArchive()) {
throw new IOException("Updating this archive format is not supported");
}
syncOpenedArchiveChanges(callback);
}
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
progressDialog.dispose(); progressDialog.dispose();
loadDirectory(targetDir, false); loadDirectory(targetDir, false);

View File

@ -1775,10 +1775,14 @@ public class MainWindow extends JFrame {
boolean background = (result == 1); boolean background = (result == 1);
if (background) { if (background) {
addOperationToQueue("Copy", "Copy %d items to %s".formatted(selectedItems.size(), targetDir.getName()), addOperationToQueue("Copy", "Copy %d items to %s".formatted(selectedItems.size(), targetDir.getName()),
(cb) -> FileOperations.copy(selectedItems, targetDir, cb), () -> sourcePanel.unselectAll(), targetPanel); (cb) -> {
FileOperations.copy(selectedItems, targetDir, cb);
syncTargetArchiveIfNeeded(targetPanel, targetDir, cb);
}, () -> sourcePanel.unselectAll(), targetPanel);
} else { } else {
performFileOperation((callback) -> { performFileOperation((callback) -> {
FileOperations.copy(selectedItems, targetDir, callback); FileOperations.copy(selectedItems, targetDir, callback);
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback);
}, "Copy completed", false, true, () -> sourcePanel.unselectAll(), targetPanel); }, "Copy completed", false, true, () -> sourcePanel.unselectAll(), targetPanel);
} }
} else { } else {
@ -1813,10 +1817,14 @@ public class MainWindow extends JFrame {
boolean background = (result == 1); boolean background = (result == 1);
if (background) { if (background) {
addOperationToQueue("Move", "Move %d items to %s".formatted(selectedItems.size(), targetDir.getName()), addOperationToQueue("Move", "Move %d items to %s".formatted(selectedItems.size(), targetDir.getName()),
(cb) -> FileOperations.move(selectedItems, targetDir, cb), activePanel, targetPanel); (cb) -> {
FileOperations.move(selectedItems, targetDir, cb);
syncTargetArchiveIfNeeded(targetPanel, targetDir, cb);
}, activePanel, targetPanel);
} else { } else {
performFileOperation((callback) -> { performFileOperation((callback) -> {
FileOperations.move(selectedItems, targetDir, callback); FileOperations.move(selectedItems, targetDir, callback);
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback);
}, "Move completed", false, true, activePanel, targetPanel); }, "Move completed", false, true, activePanel, targetPanel);
} }
} else { } else {
@ -3461,6 +3469,17 @@ public class MainWindow extends JFrame {
} }
} }
private void syncTargetArchiveIfNeeded(FilePanel targetPanel, File targetDir, FileOperations.ProgressCallback callback) throws IOException {
if (targetPanel == null || targetDir == null) return;
FilePanelTab targetTab = targetPanel.getCurrentTab();
if (targetTab == null) return;
if (!targetTab.isDirectoryInsideOpenedArchive(targetDir)) return;
if (!targetTab.canSyncOpenedArchive()) {
throw new IOException("Updating this archive format is not supported");
}
targetTab.syncOpenedArchiveChanges(callback);
}
private void updateAutoRefreshTimer() { private void updateAutoRefreshTimer() {
if (autoRefreshTimer != null) { if (autoRefreshTimer != null) {
autoRefreshTimer.stop(); autoRefreshTimer.stop();