better archive support
This commit is contained in:
parent
be40fe132f
commit
4e1b4feac7
@ -15,7 +15,7 @@ import java.io.InputStreamReader;
|
||||
*/
|
||||
public class MainApp {
|
||||
|
||||
public static final String APP_VERSION = "1.2.7";
|
||||
public static final String APP_VERSION = "1.2.8";
|
||||
|
||||
public enum OS {
|
||||
WINDOWS, LINUX, MACOS, UNKNOWN
|
||||
|
||||
@ -26,6 +26,7 @@ import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import cz.kamma.kfmanager.model.FileItem;
|
||||
import net.lingala.zip4j.ZipFile;
|
||||
@ -1094,6 +1095,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
|
||||
*/
|
||||
|
||||
@ -76,6 +76,7 @@ public class FilePanelTab extends JPanel {
|
||||
// we can cleanup older temp directories when navigation changes.
|
||||
private Path currentArchiveTempDir = null;
|
||||
private File currentArchiveSourceFile = null;
|
||||
private String currentArchivePassword = null;
|
||||
private File archiveReturnDirectory = null;
|
||||
private Point archiveReturnViewPosition = null;
|
||||
private boolean inlineRenameActive = false;
|
||||
@ -1618,7 +1619,7 @@ public class FilePanelTab extends JPanel {
|
||||
Path newPath = (newDirectory != null) ? newDirectory.toPath().toAbsolutePath().normalize() : null;
|
||||
if (newPath == null || !newPath.startsWith(currentArchiveTempDir)) {
|
||||
deleteTempDirRecursively(currentArchiveTempDir);
|
||||
currentArchiveTempDir = null;
|
||||
clearOpenedArchiveSession();
|
||||
}
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
@ -1665,6 +1666,7 @@ public class FilePanelTab extends JPanel {
|
||||
if (archive == null || !archive.isFile()) return null;
|
||||
try {
|
||||
Path tempDir = Files.createTempDirectory("kfmanager-archive-");
|
||||
final String[] usedPassword = new String[1];
|
||||
FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() {
|
||||
@Override
|
||||
public void onProgress(long current, long total, String currentFile) {}
|
||||
@ -1701,6 +1703,7 @@ public class FilePanelTab extends JPanel {
|
||||
Object selectedValue = pane.getValue();
|
||||
if (selectedValue != null && (Integer) selectedValue == JOptionPane.OK_OPTION) {
|
||||
result[0] = new String(pf.getPassword());
|
||||
usedPassword[0] = result[0];
|
||||
}
|
||||
};
|
||||
|
||||
@ -1715,12 +1718,14 @@ public class FilePanelTab extends JPanel {
|
||||
return result[0];
|
||||
}
|
||||
});
|
||||
currentArchivePassword = usedPassword[0];
|
||||
return tempDir;
|
||||
} catch (Exception ex) {
|
||||
// extraction failed; attempt best-effort cleanup
|
||||
try {
|
||||
if (currentArchiveTempDir != null) deleteTempDirRecursively(currentArchiveTempDir);
|
||||
} catch (Exception ignore) {}
|
||||
currentArchivePassword = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -1738,6 +1743,28 @@ public class FilePanelTab extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
int selectedRow = fileTable.getSelectedRow();
|
||||
if (selectedRow >= 0) {
|
||||
@ -2270,8 +2297,7 @@ public class FilePanelTab extends JPanel {
|
||||
String archiveName = currentArchiveSourceFile.getName();
|
||||
// cleanup temp dir before switching back
|
||||
deleteTempDirRecursively(currentArchiveTempDir);
|
||||
currentArchiveTempDir = null;
|
||||
currentArchiveSourceFile = null;
|
||||
clearOpenedArchiveSession();
|
||||
loadDirectory(parent, false, true, () -> {
|
||||
// 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.
|
||||
@ -2384,6 +2410,12 @@ public class FilePanelTab extends JPanel {
|
||||
archiveReturnViewPosition = null;
|
||||
}
|
||||
|
||||
private void clearOpenedArchiveSession() {
|
||||
currentArchiveTempDir = null;
|
||||
currentArchiveSourceFile = null;
|
||||
currentArchivePassword = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public wrapper to select an item by name from outside this class.
|
||||
* Useful for other UI components to request focusing a specific file.
|
||||
@ -2738,6 +2770,13 @@ public class FilePanelTab extends JPanel {
|
||||
} else {
|
||||
FileOperations.copy(itemsToPaste, targetDir, callback);
|
||||
}
|
||||
|
||||
if (isDirectoryInsideOpenedArchive(targetDir)) {
|
||||
if (!canSyncOpenedArchive()) {
|
||||
throw new IOException("Updating this archive format is not supported");
|
||||
}
|
||||
syncOpenedArchiveChanges(callback);
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
progressDialog.dispose();
|
||||
loadDirectory(targetDir, false);
|
||||
|
||||
@ -1775,10 +1775,14 @@ public class MainWindow extends JFrame {
|
||||
boolean background = (result == 1);
|
||||
if (background) {
|
||||
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 {
|
||||
performFileOperation((callback) -> {
|
||||
FileOperations.copy(selectedItems, targetDir, callback);
|
||||
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback);
|
||||
}, "Copy completed", false, true, () -> sourcePanel.unselectAll(), targetPanel);
|
||||
}
|
||||
} else {
|
||||
@ -1813,10 +1817,14 @@ public class MainWindow extends JFrame {
|
||||
boolean background = (result == 1);
|
||||
if (background) {
|
||||
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 {
|
||||
performFileOperation((callback) -> {
|
||||
FileOperations.move(selectedItems, targetDir, callback);
|
||||
syncTargetArchiveIfNeeded(targetPanel, targetDir, callback);
|
||||
}, "Move completed", false, true, activePanel, targetPanel);
|
||||
}
|
||||
} 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() {
|
||||
if (autoRefreshTimer != null) {
|
||||
autoRefreshTimer.stop();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user