zip rewritten to memory
This commit is contained in:
parent
d51b53bb4b
commit
07434a9fd6
@ -12,9 +12,14 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
@ -25,6 +30,12 @@ import org.apache.commons.compress.archivers.sevenz.SevenZFile;
|
|||||||
import cz.kamma.kfmanager.model.FileItem;
|
import cz.kamma.kfmanager.model.FileItem;
|
||||||
import cz.kamma.kfmanager.model.FtpProfile;
|
import cz.kamma.kfmanager.model.FtpProfile;
|
||||||
import net.lingala.zip4j.ZipFile;
|
import net.lingala.zip4j.ZipFile;
|
||||||
|
import net.lingala.zip4j.io.outputstream.ZipOutputStream;
|
||||||
|
import net.lingala.zip4j.model.ZipParameters;
|
||||||
|
import net.lingala.zip4j.model.enums.AesKeyStrength;
|
||||||
|
import net.lingala.zip4j.model.enums.CompressionLevel;
|
||||||
|
import net.lingala.zip4j.model.enums.CompressionMethod;
|
||||||
|
import net.lingala.zip4j.model.enums.EncryptionMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for file operations - copy, move, delete, etc.
|
* Service for file operations - copy, move, delete, etc.
|
||||||
@ -1055,6 +1066,67 @@ public class FileOperations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract ZIP/JAR/WAR by first loading archive bytes to memory and then
|
||||||
|
* reading entries from ZipInputStream.
|
||||||
|
*/
|
||||||
|
public static void extractZipArchiveFromMemory(File archive, File targetDir, ProgressCallback callback) throws IOException {
|
||||||
|
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||||
|
throw new IOException("Failed to create target directory: " + targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = archive.getName().toLowerCase();
|
||||||
|
if (!(name.endsWith(".zip") || name.endsWith(".jar") || name.endsWith(".war"))) {
|
||||||
|
throw new IOException("Unsupported archive format for in-memory zip extraction: " + archive.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypted archives need zip4j handling with password prompt.
|
||||||
|
try (ZipFile zipFile = new ZipFile(archive)) {
|
||||||
|
if (zipFile.isEncrypted()) {
|
||||||
|
extractZip(archive, targetDir, callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] zipBytes = Files.readAllBytes(archive.toPath());
|
||||||
|
long total = countZipEntries(zipBytes);
|
||||||
|
long current = 0;
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
|
|
||||||
|
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
|
if (callback != null && callback.isCancelled()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
File targetFile = secureZipTarget(targetDir, entry.getName());
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
targetFile.mkdirs();
|
||||||
|
} else {
|
||||||
|
File parent = targetFile.getParentFile();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.mkdirs();
|
||||||
|
}
|
||||||
|
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(targetFile), 65536)) {
|
||||||
|
int len;
|
||||||
|
while ((len = zis.read(buffer)) > 0) {
|
||||||
|
if (callback != null && callback.isCancelled()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(current, total, entry.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void extractZip(File archive, File targetDir, ProgressCallback callback) throws IOException {
|
private static void extractZip(File archive, File targetDir, ProgressCallback callback) throws IOException {
|
||||||
String password = null;
|
String password = null;
|
||||||
try (ZipFile zipFile = new ZipFile(archive)) {
|
try (ZipFile zipFile = new ZipFile(archive)) {
|
||||||
@ -1077,6 +1149,26 @@ public class FileOperations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long countZipEntries(byte[] zipBytes) throws IOException {
|
||||||
|
long total = 0;
|
||||||
|
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||||
|
while (zis.getNextEntry() != null) {
|
||||||
|
total++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File secureZipTarget(File targetDir, String entryName) throws IOException {
|
||||||
|
File targetFile = new File(targetDir, entryName);
|
||||||
|
String targetDirPath = targetDir.getCanonicalPath() + File.separator;
|
||||||
|
String targetFilePath = targetFile.getCanonicalPath();
|
||||||
|
if (!targetFilePath.startsWith(targetDirPath) && !targetFilePath.equals(targetDir.getCanonicalPath())) {
|
||||||
|
throw new IOException("Blocked unsafe ZIP entry path: " + entryName);
|
||||||
|
}
|
||||||
|
return targetFile;
|
||||||
|
}
|
||||||
|
|
||||||
private static void extract7z(File archive, File targetDir, ProgressCallback callback) throws IOException {
|
private static void extract7z(File archive, File targetDir, ProgressCallback callback) throws IOException {
|
||||||
String password = null;
|
String password = null;
|
||||||
SevenZFile szf = null;
|
SevenZFile szf = null;
|
||||||
@ -1208,35 +1300,74 @@ public class FileOperations {
|
|||||||
throw new IOException("Source directory for rewrite does not exist");
|
throw new IOException("Source directory for rewrite does not exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetArchive.exists()) {
|
List<File> allEntries = listArchiveEntries(sourceDir);
|
||||||
targetArchive.delete();
|
long total = allEntries.size();
|
||||||
|
long current = 0;
|
||||||
|
byte[] buffer = new byte[65536];
|
||||||
|
|
||||||
|
ByteArrayOutputStream archiveBuffer = new ByteArrayOutputStream();
|
||||||
|
char[] passwordChars = (password != null && !password.isEmpty()) ? password.toCharArray() : null;
|
||||||
|
try (ZipOutputStream zipStream = (passwordChars != null)
|
||||||
|
? new ZipOutputStream(archiveBuffer, passwordChars)
|
||||||
|
: new ZipOutputStream(archiveBuffer)) {
|
||||||
|
|
||||||
|
for (File entry : allEntries) {
|
||||||
|
if (callback != null && callback.isCancelled()) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (ZipFile zipFile = new ZipFile(targetArchive)) {
|
String relativePath = sourceDir.toPath().relativize(entry.toPath()).toString().replace(File.separatorChar, '/');
|
||||||
if (password != null && !password.isEmpty()) {
|
if (relativePath.isEmpty()) {
|
||||||
zipFile.setPassword(password.toCharArray());
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
net.lingala.zip4j.model.ZipParameters params = new net.lingala.zip4j.model.ZipParameters();
|
ZipParameters params = new ZipParameters();
|
||||||
if (password != null && !password.isEmpty()) {
|
params.setCompressionMethod(CompressionMethod.DEFLATE);
|
||||||
|
params.setCompressionLevel(CompressionLevel.NORMAL);
|
||||||
|
params.setFileNameInZip(entry.isDirectory() ? relativePath + "/" : relativePath);
|
||||||
|
|
||||||
|
if (passwordChars != null && !entry.isDirectory()) {
|
||||||
params.setEncryptFiles(true);
|
params.setEncryptFiles(true);
|
||||||
params.setEncryptionMethod(net.lingala.zip4j.model.enums.EncryptionMethod.AES);
|
params.setEncryptionMethod(EncryptionMethod.AES);
|
||||||
params.setAesKeyStrength(net.lingala.zip4j.model.enums.AesKeyStrength.KEY_STRENGTH_256);
|
params.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
|
||||||
}
|
}
|
||||||
|
|
||||||
File[] children = sourceDir.listFiles();
|
zipStream.putNextEntry(params);
|
||||||
if (children != null) {
|
if (!entry.isDirectory()) {
|
||||||
for (File child : children) {
|
try (InputStream in = new BufferedInputStream(new FileInputStream(entry), 65536)) {
|
||||||
if (callback != null && callback.isCancelled()) break;
|
int len;
|
||||||
if (child.isDirectory()) {
|
while ((len = in.read(buffer)) > 0) {
|
||||||
zipFile.addFolder(child, params);
|
if (callback != null && callback.isCancelled()) {
|
||||||
} else {
|
break;
|
||||||
zipFile.addFile(child, params);
|
}
|
||||||
|
zipStream.write(buffer, 0, len);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
zipStream.closeEntry();
|
||||||
|
|
||||||
|
current++;
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onProgress(current, total, relativePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(targetArchive), 65536)) {
|
||||||
|
archiveBuffer.writeTo(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<File> listArchiveEntries(File sourceDir) throws IOException {
|
||||||
|
List<File> entries = new ArrayList<>();
|
||||||
|
try (Stream<Path> stream = Files.walk(sourceDir.toPath())) {
|
||||||
|
stream
|
||||||
|
.filter(path -> !path.equals(sourceDir.toPath()))
|
||||||
|
.sorted(Comparator.comparingInt(path -> path.getNameCount()))
|
||||||
|
.forEach(path -> entries.add(path.toFile()));
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
public enum OverwriteResponse {
|
public enum OverwriteResponse {
|
||||||
YES, YES_TO_ALL, NO, NO_TO_ALL, CANCEL
|
YES, YES_TO_ALL, NO, NO_TO_ALL, CANCEL
|
||||||
|
|||||||
@ -38,10 +38,12 @@ import java.util.Map;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.nio.file.attribute.PosixFileAttributes;
|
import java.nio.file.attribute.PosixFileAttributes;
|
||||||
import java.nio.file.attribute.PosixFilePermissions;
|
import java.nio.file.attribute.PosixFilePermissions;
|
||||||
import java.nio.file.attribute.DosFileAttributes;
|
import java.nio.file.attribute.DosFileAttributes;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Single tab in a panel - displays the contents of one directory
|
* Single tab in a panel - displays the contents of one directory
|
||||||
@ -4681,10 +4683,11 @@ public class FilePanelTab extends JPanel {
|
|||||||
|
|
||||||
Thread extractionThread = new Thread(() -> {
|
Thread extractionThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Path tempDir = java.nio.file.Files.createTempDirectory("kfmanager-archive-");
|
Path tempDir = createArchiveWorkspaceDir();
|
||||||
final String[] usedPassword = new String[1];
|
final String[] usedPassword = new String[1];
|
||||||
|
|
||||||
FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() {
|
String archiveLower = archive.getName().toLowerCase();
|
||||||
|
FileOperations.ProgressCallback callback = new FileOperations.ProgressCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(long current, long total, String currentFile) {
|
public void onProgress(long current, long total, String currentFile) {
|
||||||
SwingUtilities.invokeLater(() -> {
|
SwingUtilities.invokeLater(() -> {
|
||||||
@ -4763,7 +4766,13 @@ public class FilePanelTab extends JPanel {
|
|||||||
public FileOperations.SymlinkResponse confirmSymlink(File file) {
|
public FileOperations.SymlinkResponse confirmSymlink(File file) {
|
||||||
return FileOperations.SymlinkResponse.FOLLOW;
|
return FileOperations.SymlinkResponse.FOLLOW;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (archiveLower.endsWith(".zip") || archiveLower.endsWith(".jar") || archiveLower.endsWith(".war")) {
|
||||||
|
FileOperations.extractZipArchiveFromMemory(archive, tempDir.toFile(), callback);
|
||||||
|
} else {
|
||||||
|
FileOperations.extractArchive(archive, tempDir.toFile(), callback);
|
||||||
|
}
|
||||||
|
|
||||||
currentArchivePassword = usedPassword[0];
|
currentArchivePassword = usedPassword[0];
|
||||||
|
|
||||||
@ -4785,4 +4794,13 @@ public class FilePanelTab extends JPanel {
|
|||||||
extractionThread.setDaemon(false);
|
extractionThread.setDaemon(false);
|
||||||
extractionThread.start();
|
extractionThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path createArchiveWorkspaceDir() throws IOException {
|
||||||
|
String home = System.getProperty("user.home");
|
||||||
|
Path base = Paths.get(home, ".kfmanager", "archive-work");
|
||||||
|
Files.createDirectories(base);
|
||||||
|
Path dir = base.resolve("session-" + UUID.randomUUID());
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user