added support for encrypted archives
This commit is contained in:
parent
6cdcb3bbfe
commit
72661040df
10
pom.xml
10
pom.xml
@ -91,11 +91,21 @@
|
|||||||
<artifactId>commons-compress</artifactId>
|
<artifactId>commons-compress</artifactId>
|
||||||
<version>1.21</version>
|
<version>1.21</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.tukaani</groupId>
|
||||||
|
<artifactId>xz</artifactId>
|
||||||
|
<version>1.9</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.junrar</groupId>
|
<groupId>com.github.junrar</groupId>
|
||||||
<artifactId>junrar</artifactId>
|
<artifactId>junrar</artifactId>
|
||||||
<version>7.4.1</version>
|
<version>7.4.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.lingala.zip4j</groupId>
|
||||||
|
<artifactId>zip4j</artifactId>
|
||||||
|
<version>2.11.5</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.formdev</groupId>
|
<groupId>com.formdev</groupId>
|
||||||
<artifactId>flatlaf</artifactId>
|
<artifactId>flatlaf</artifactId>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import java.io.InputStreamReader;
|
|||||||
*/
|
*/
|
||||||
public class MainApp {
|
public class MainApp {
|
||||||
|
|
||||||
public static final String APP_VERSION = "1.1.1";
|
public static final String APP_VERSION = "1.1.2";
|
||||||
|
|
||||||
public enum OS {
|
public enum OS {
|
||||||
WINDOWS, LINUX, MACOS, UNKNOWN
|
WINDOWS, LINUX, MACOS, UNKNOWN
|
||||||
|
|||||||
@ -13,6 +13,9 @@ import java.util.Set;
|
|||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.zip.*;
|
import java.util.zip.*;
|
||||||
|
|
||||||
|
import net.lingala.zip4j.ZipFile;
|
||||||
|
import net.lingala.zip4j.model.FileHeader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for file operations - copy, move, delete, etc.
|
* Service for file operations - copy, move, delete, etc.
|
||||||
*/
|
*/
|
||||||
@ -1106,49 +1109,77 @@ public class FileOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void extractSevenZ(File archive, File targetDir, ProgressCallback callback) throws IOException {
|
private static void extractSevenZ(File archive, File targetDir, ProgressCallback callback) throws IOException {
|
||||||
try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) {
|
char[] password = null;
|
||||||
org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
|
while (true) {
|
||||||
long current = 0;
|
try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile =
|
||||||
while ((entry = sevenZFile.getNextEntry()) != null) {
|
(password == null) ? new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive) :
|
||||||
current++;
|
new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive, password)) {
|
||||||
if (callback != null) callback.onProgress(current, -1, entry.getName());
|
org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
|
||||||
|
long current = 0;
|
||||||
File newFile = new File(targetDir, entry.getName());
|
while ((entry = sevenZFile.getNextEntry()) != null) {
|
||||||
if (entry.isDirectory()) {
|
current++;
|
||||||
newFile.mkdirs();
|
if (callback != null) callback.onProgress(current, -1, entry.getName());
|
||||||
} else {
|
|
||||||
newFile.getParentFile().mkdirs();
|
File newFile = new File(targetDir, entry.getName());
|
||||||
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
|
if (entry.isDirectory()) {
|
||||||
byte[] buffer = new byte[8192];
|
newFile.mkdirs();
|
||||||
int bytesRead;
|
} else {
|
||||||
while ((bytesRead = sevenZFile.read(buffer)) != -1) {
|
newFile.getParentFile().mkdirs();
|
||||||
os.write(buffer, 0, bytesRead);
|
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = sevenZFile.read(buffer)) != -1) {
|
||||||
|
os.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (e.getMessage() != null && (e.getMessage().toLowerCase().contains("password") || e.getMessage().toLowerCase().contains("encrypted"))) {
|
||||||
|
String pass = (callback != null) ? callback.requestPassword(archive.getName()) : null;
|
||||||
|
if (pass == null) throw new IOException("Password required for " + archive.getName());
|
||||||
|
password = pass.toCharArray();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void extractRar(File archiveFile, File targetDir, ProgressCallback callback) throws Exception {
|
private static void extractRar(File archiveFile, File targetDir, ProgressCallback callback) throws Exception {
|
||||||
try (com.github.junrar.Archive archive = new com.github.junrar.Archive(archiveFile)) {
|
String password = null;
|
||||||
com.github.junrar.rarfile.FileHeader fh = archive.nextFileHeader();
|
while (true) {
|
||||||
long current = 0;
|
try (com.github.junrar.Archive archive = (password == null) ? new com.github.junrar.Archive(archiveFile) : new com.github.junrar.Archive(archiveFile, password)) {
|
||||||
while (fh != null) {
|
if (archive.isEncrypted() && password == null) {
|
||||||
current++;
|
password = (callback != null) ? callback.requestPassword(archiveFile.getName()) : null;
|
||||||
String entryName = fh.getFileName().replace('\\', '/');
|
if (password == null) throw new IOException("Password required");
|
||||||
if (callback != null) callback.onProgress(current, -1, entryName);
|
continue;
|
||||||
|
|
||||||
File newFile = new File(targetDir, entryName);
|
|
||||||
if (fh.isDirectory()) {
|
|
||||||
newFile.mkdirs();
|
|
||||||
} else {
|
|
||||||
newFile.getParentFile().mkdirs();
|
|
||||||
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
|
|
||||||
archive.extractFile(fh, os);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fh = archive.nextFileHeader();
|
com.github.junrar.rarfile.FileHeader fh = archive.nextFileHeader();
|
||||||
|
long current = 0;
|
||||||
|
while (fh != null) {
|
||||||
|
if (fh.isEncrypted() && password == null) {
|
||||||
|
password = (callback != null) ? callback.requestPassword(archiveFile.getName()) : null;
|
||||||
|
if (password == null) throw new IOException("Password required");
|
||||||
|
break; // exit inner loop to reopen with password
|
||||||
|
}
|
||||||
|
current++;
|
||||||
|
String entryName = fh.getFileName().replace('\\', '/');
|
||||||
|
if (callback != null) callback.onProgress(current, -1, entryName);
|
||||||
|
|
||||||
|
File newFile = new File(targetDir, entryName);
|
||||||
|
if (fh.isDirectory()) {
|
||||||
|
newFile.mkdirs();
|
||||||
|
} else {
|
||||||
|
newFile.getParentFile().mkdirs();
|
||||||
|
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
|
||||||
|
archive.extractFile(fh, os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fh = archive.nextFileHeader();
|
||||||
|
}
|
||||||
|
if (fh == null) break; // Finished successfully
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1161,39 +1192,71 @@ public class FileOperations {
|
|||||||
Files.createDirectories(targetDirectory.toPath());
|
Files.createDirectories(targetDirectory.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
try (org.apache.commons.compress.archivers.zip.ZipFile zf = new org.apache.commons.compress.archivers.zip.ZipFile(zipFile)) {
|
ZipFile zf = null;
|
||||||
java.util.List<org.apache.commons.compress.archivers.zip.ZipArchiveEntry> entries = java.util.Collections.list(zf.getEntries());
|
String password = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
zf = new ZipFile(zipFile);
|
||||||
|
|
||||||
|
// Check if archive is encrypted and request password
|
||||||
|
if (zf.isEncrypted()) {
|
||||||
|
password = (callback != null) ? callback.requestPassword(zipFile.getName()) : null;
|
||||||
|
if (password == null) {
|
||||||
|
throw new IOException("Password required for " + zipFile.getName());
|
||||||
|
}
|
||||||
|
zf.setPassword(password.toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileHeader> entries = zf.getFileHeaders();
|
||||||
long totalItems = entries.size();
|
long totalItems = entries.size();
|
||||||
long currentItem = 0;
|
long currentItem = 0;
|
||||||
|
|
||||||
for (org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry : entries) {
|
for (FileHeader entry : entries) {
|
||||||
currentItem++;
|
currentItem++;
|
||||||
File newFile = new File(targetDirectory, entry.getName());
|
File newFile = new File(targetDirectory, entry.getFileName());
|
||||||
|
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onProgress(currentItem, totalItems, entry.getName());
|
callback.onProgress(currentItem, totalItems, entry.getFileName());
|
||||||
|
if (callback.isCancelled()) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
try {
|
||||||
if (!newFile.isDirectory() && !newFile.mkdirs()) {
|
if (entry.isDirectory()) {
|
||||||
throw new IOException("Failed to create directory " + newFile);
|
if (!newFile.isDirectory() && !newFile.mkdirs()) {
|
||||||
|
throw new IOException("Failed to create directory " + newFile);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
File parent = newFile.getParentFile();
|
||||||
|
if (parent != null && !parent.exists()) {
|
||||||
|
parent.mkdirs();
|
||||||
|
}
|
||||||
|
zf.extractFile(entry, targetDirectory.getAbsolutePath());
|
||||||
}
|
}
|
||||||
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
|
} catch (Exception e) {
|
||||||
} else {
|
// Check if this is a password-related error on an encrypted file
|
||||||
// create parent directories if they don't exist
|
if (entry.isEncrypted() && e.getMessage() != null &&
|
||||||
File parent = newFile.getParentFile();
|
(e.getMessage().toLowerCase().contains("password") ||
|
||||||
if (parent != null && !parent.exists()) {
|
e.getMessage().toLowerCase().contains("decrypt") ||
|
||||||
parent.mkdirs();
|
e.getMessage().toLowerCase().contains("checksum"))) {
|
||||||
|
throw new IOException("Incorrect password for encrypted archive", e);
|
||||||
}
|
}
|
||||||
|
// For other errors, skip the file and continue
|
||||||
if (newFile.exists() && newFile.isDirectory()) {
|
System.err.println("Warning: Failed to extract " + entry.getFileName() + ": " + e.getMessage());
|
||||||
deleteDirectoryInternal(newFile.toPath());
|
}
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
try (InputStream is = zf.getInputStream(entry)) {
|
if (e instanceof IOException && e.getMessage() != null &&
|
||||||
Files.copy(is, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
(e.getMessage().contains("Password required") ||
|
||||||
}
|
e.getMessage().contains("Incorrect password"))) {
|
||||||
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
|
throw (IOException) e;
|
||||||
|
}
|
||||||
|
throw new IOException("Failed to unzip: " + e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
if (zf != null) {
|
||||||
|
try {
|
||||||
|
zf.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ignore errors during close
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1223,6 +1286,7 @@ public class FileOperations {
|
|||||||
default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; }
|
default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; }
|
||||||
default SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; }
|
default SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; }
|
||||||
default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; }
|
default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; }
|
||||||
|
default String requestPassword(String archiveName) { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1310,7 +1310,35 @@ 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-");
|
||||||
FileOperations.extractArchive(archive, tempDir.toFile(), null);
|
FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() {
|
||||||
|
@Override
|
||||||
|
public void onProgress(long current, long total, String currentFile) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String requestPassword(String archiveName) {
|
||||||
|
final String[] result = new String[1];
|
||||||
|
try {
|
||||||
|
if (SwingUtilities.isEventDispatchThread()) {
|
||||||
|
JPasswordField pf = new JPasswordField();
|
||||||
|
int ok = JOptionPane.showConfirmDialog(FilePanelTab.this, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
|
||||||
|
if (ok == JOptionPane.OK_OPTION) {
|
||||||
|
result[0] = new String(pf.getPassword());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SwingUtilities.invokeAndWait(() -> {
|
||||||
|
JPasswordField pf = new JPasswordField();
|
||||||
|
int ok = JOptionPane.showConfirmDialog(FilePanelTab.this, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
|
||||||
|
if (ok == JOptionPane.OK_OPTION) {
|
||||||
|
result[0] = new String(pf.getPassword());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
result[0] = null;
|
||||||
|
}
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
return tempDir;
|
return tempDir;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// extraction failed; attempt best-effort cleanup
|
// extraction failed; attempt best-effort cleanup
|
||||||
|
|||||||
@ -2698,6 +2698,25 @@ public class MainWindow extends JFrame {
|
|||||||
return result[0];
|
return result[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String requestPassword(String archiveName) {
|
||||||
|
final String[] result = new String[1];
|
||||||
|
try {
|
||||||
|
SwingUtilities.invokeAndWait(() -> {
|
||||||
|
JPasswordField pf = new JPasswordField();
|
||||||
|
int ok = JOptionPane.showConfirmDialog(progressDialog, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
|
||||||
|
if (ok == JOptionPane.OK_OPTION) {
|
||||||
|
result[0] = new String(pf.getPassword());
|
||||||
|
} else {
|
||||||
|
result[0] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
result[0] = null;
|
||||||
|
}
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileOperations.SymlinkResponse confirmSymlink(File symlink) {
|
public FileOperations.SymlinkResponse confirmSymlink(File symlink) {
|
||||||
final FileOperations.SymlinkResponse[] result = new FileOperations.SymlinkResponse[1];
|
final FileOperations.SymlinkResponse[] result = new FileOperations.SymlinkResponse[1];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user