better archive support
This commit is contained in:
parent
be40fe132f
commit
4e1b4feac7
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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
|
* 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.
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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() {
|
private void openSelectedItem() {
|
||||||
int selectedRow = fileTable.getSelectedRow();
|
int selectedRow = fileTable.getSelectedRow();
|
||||||
if (selectedRow >= 0) {
|
if (selectedRow >= 0) {
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user