Add JVM hybrid-extract retry and clean up Java extractor
- Add automatic retry with 3s delay when JVM extractor fails with "codecs" or "can't be opened" error during hybrid-extract mode (handles transient Windows file locks after download completion) - Log archive file size before JVM extraction in hybrid mode - Remove unused ArchiveFormat import, RAR_MULTIPART_RE/RAR_OLDSPLIT_RE patterns, and hasOldStyleRarSplits() method from Java extractor - Keep simple openSevenZipArchive with currentVolumeName tracking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f9b0bbe676
commit
353cef7dbd
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -11,7 +11,6 @@ import net.sf.sevenzipjbinding.IInStream;
|
|||||||
import net.sf.sevenzipjbinding.ISequentialOutStream;
|
import net.sf.sevenzipjbinding.ISequentialOutStream;
|
||||||
import net.sf.sevenzipjbinding.ICryptoGetTextPassword;
|
import net.sf.sevenzipjbinding.ICryptoGetTextPassword;
|
||||||
import net.sf.sevenzipjbinding.PropID;
|
import net.sf.sevenzipjbinding.PropID;
|
||||||
import net.sf.sevenzipjbinding.ArchiveFormat;
|
|
||||||
import net.sf.sevenzipjbinding.SevenZip;
|
import net.sf.sevenzipjbinding.SevenZip;
|
||||||
import net.sf.sevenzipjbinding.SevenZipException;
|
import net.sf.sevenzipjbinding.SevenZipException;
|
||||||
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
|
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
|
||||||
@ -43,8 +42,6 @@ public final class JBindExtractorMain {
|
|||||||
private static final Pattern NUMBERED_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.zip\\.\\d{3}$");
|
private static final Pattern NUMBERED_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.zip\\.\\d{3}$");
|
||||||
private static final Pattern OLD_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.z\\d{2,3}$");
|
private static final Pattern OLD_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.z\\d{2,3}$");
|
||||||
private static final Pattern SEVEN_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.7z\\.001$");
|
private static final Pattern SEVEN_ZIP_SPLIT_RE = Pattern.compile("(?i).*\\.7z\\.001$");
|
||||||
private static final Pattern RAR_MULTIPART_RE = Pattern.compile("(?i).*\\.part\\d+\\.rar$");
|
|
||||||
private static final Pattern RAR_OLDSPLIT_RE = Pattern.compile("(?i).*\\.r\\d{2,3}$");
|
|
||||||
private static volatile boolean sevenZipInitialized = false;
|
private static volatile boolean sevenZipInitialized = false;
|
||||||
|
|
||||||
private JBindExtractorMain() {
|
private JBindExtractorMain() {
|
||||||
@ -330,78 +327,21 @@ public final class JBindExtractorMain {
|
|||||||
SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword);
|
SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword);
|
||||||
|
|
||||||
// VolumedArchiveInStream is ONLY for .7z.001 split archives.
|
// VolumedArchiveInStream is ONLY for .7z.001 split archives.
|
||||||
// It internally checks for the ".7z.001" suffix and rejects everything else.
|
|
||||||
if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) {
|
if (SEVEN_ZIP_SPLIT_RE.matcher(nameLower).matches()) {
|
||||||
VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback);
|
VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback);
|
||||||
IInArchive archive = SevenZip.openInArchive(null, volumed, callback);
|
IInArchive archive = SevenZip.openInArchive(null, volumed, callback);
|
||||||
return new SevenZipArchiveContext(archive, null, volumed, callback);
|
return new SevenZipArchiveContext(archive, null, volumed, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi-part RAR (.part1.rar, .part2.rar or old-style .rar/.r01/.r02):
|
// All other archives (including multi-part RAR): use RandomAccessFileInStream
|
||||||
// The first stream MUST be obtained via the callback so the volume name
|
// with auto-detection. The IArchiveOpenVolumeCallback handles additional
|
||||||
// tracker is properly initialized. 7z-JBinding uses getProperty(NAME)
|
// volumes when 7z-JBinding requests them.
|
||||||
// to compute subsequent volume filenames.
|
|
||||||
boolean isMultiPartRar = RAR_MULTIPART_RE.matcher(nameLower).matches()
|
|
||||||
|| hasOldStyleRarSplits(archiveFile);
|
|
||||||
|
|
||||||
if (isMultiPartRar) {
|
|
||||||
IInStream inStream = callback.getStream(archiveFile.getAbsolutePath());
|
|
||||||
if (inStream == null) {
|
|
||||||
throw new IOException("Archiv konnte nicht geoeffnet werden: " + archiveFile.getAbsolutePath());
|
|
||||||
}
|
|
||||||
// Try RAR5 first (modern), then RAR4, then auto-detect
|
|
||||||
Exception lastError = null;
|
|
||||||
ArchiveFormat[] rarFormats = { ArchiveFormat.RAR5, ArchiveFormat.RAR, null };
|
|
||||||
for (ArchiveFormat fmt : rarFormats) {
|
|
||||||
try {
|
|
||||||
inStream.seek(0L, 0);
|
|
||||||
IInArchive archive = SevenZip.openInArchive(fmt, inStream, callback);
|
|
||||||
return new SevenZipArchiveContext(archive, null, null, callback);
|
|
||||||
} catch (Exception e) {
|
|
||||||
lastError = e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback.close();
|
|
||||||
throw lastError != null ? lastError : new IOException("Archiv konnte nicht geoeffnet werden");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single-file archives: open directly with auto-detection
|
|
||||||
RandomAccessFile raf = new RandomAccessFile(archiveFile, "r");
|
RandomAccessFile raf = new RandomAccessFile(archiveFile, "r");
|
||||||
RandomAccessFileInStream stream = new RandomAccessFileInStream(raf);
|
RandomAccessFileInStream stream = new RandomAccessFileInStream(raf);
|
||||||
IInArchive archive = SevenZip.openInArchive(null, stream, callback);
|
IInArchive archive = SevenZip.openInArchive(null, stream, callback);
|
||||||
return new SevenZipArchiveContext(archive, stream, null, callback);
|
return new SevenZipArchiveContext(archive, stream, null, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hasOldStyleRarSplits(File archiveFile) {
|
|
||||||
// Old-style RAR splits: main.rar + main.r01, main.r02, ...
|
|
||||||
String name = archiveFile.getName();
|
|
||||||
if (!name.toLowerCase(Locale.ROOT).endsWith(".rar")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
File parent = archiveFile.getParentFile();
|
|
||||||
if (parent == null || !parent.exists()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
File[] siblings = parent.listFiles();
|
|
||||||
if (siblings == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String stem = name.substring(0, name.length() - 4);
|
|
||||||
for (File sibling : siblings) {
|
|
||||||
if (!sibling.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String sibName = sibling.getName();
|
|
||||||
if (sibName.length() > stem.length() + 1 && sibName.substring(0, stem.length()).equalsIgnoreCase(stem)) {
|
|
||||||
String suffix = sibName.substring(stem.length());
|
|
||||||
if (RAR_OLDSPLIT_RE.matcher(suffix).matches() || suffix.toLowerCase(Locale.ROOT).matches("\\.r\\d{2,3}")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isWrongPassword(ZipException error, boolean encrypted) {
|
private static boolean isWrongPassword(ZipException error, boolean encrypted) {
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -1169,7 +1169,17 @@ async function runExternalExtract(
|
|||||||
}
|
}
|
||||||
logger.warn(`JVM-Extractor nicht verfügbar, nutze Legacy-Extractor: ${path.basename(archivePath)}`);
|
logger.warn(`JVM-Extractor nicht verfügbar, nutze Legacy-Extractor: ${path.basename(archivePath)}`);
|
||||||
} else {
|
} else {
|
||||||
|
if (hybridMode) {
|
||||||
|
try {
|
||||||
|
const archiveStat = await fs.promises.stat(archivePath);
|
||||||
|
logger.info(`Hybrid-Extract JVM: ${path.basename(archivePath)} (${(archiveStat.size / 1048576).toFixed(1)} MB)`);
|
||||||
|
} catch (statErr) {
|
||||||
|
logger.warn(`Hybrid-Extract JVM: Archiv nicht zugreifbar: ${path.basename(archivePath)} — ${String(statErr)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
logger.info(`JVM-Extractor aktiv (${layout.rootDir}): ${path.basename(archivePath)}`);
|
logger.info(`JVM-Extractor aktiv (${layout.rootDir}): ${path.basename(archivePath)}`);
|
||||||
|
const maxJvmAttempts = hybridMode ? 2 : 1;
|
||||||
|
for (let jvmAttempt = 1; jvmAttempt <= maxJvmAttempts; jvmAttempt++) {
|
||||||
const jvmResult = await runJvmExtractCommand(
|
const jvmResult = await runJvmExtractCommand(
|
||||||
layout,
|
layout,
|
||||||
archivePath,
|
archivePath,
|
||||||
@ -1182,6 +1192,9 @@ async function runExternalExtract(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (jvmResult.ok) {
|
if (jvmResult.ok) {
|
||||||
|
if (jvmAttempt > 1) {
|
||||||
|
logger.info(`JVM-Extractor Retry #${jvmAttempt - 1} erfolgreich: ${path.basename(archivePath)}`);
|
||||||
|
}
|
||||||
logger.info(`Entpackt via ${jvmResult.backend || "jvm"} [CPU=Idle, I/O=Normal, single-thread]: ${path.basename(archivePath)}`);
|
logger.info(`Entpackt via ${jvmResult.backend || "jvm"} [CPU=Idle, I/O=Normal, single-thread]: ${path.basename(archivePath)}`);
|
||||||
return jvmResult.usedPassword;
|
return jvmResult.usedPassword;
|
||||||
}
|
}
|
||||||
@ -1193,6 +1206,16 @@ async function runExternalExtract(
|
|||||||
}
|
}
|
||||||
|
|
||||||
jvmFailureReason = jvmResult.errorText || "JVM-Extractor fehlgeschlagen";
|
jvmFailureReason = jvmResult.errorText || "JVM-Extractor fehlgeschlagen";
|
||||||
|
|
||||||
|
// In hybrid mode, retry once on "codecs" / "can't be opened" errors —
|
||||||
|
// these can be caused by transient Windows file locks right after download completion.
|
||||||
|
const isTransientOpen = jvmFailureReason.includes("codecs") || jvmFailureReason.includes("can't be opened");
|
||||||
|
if (hybridMode && isTransientOpen && jvmAttempt < maxJvmAttempts) {
|
||||||
|
logger.warn(`JVM-Extractor Hybrid-Retry: ${jvmFailureReason} — warte 3s vor Versuch #${jvmAttempt + 1}: ${path.basename(archivePath)}`);
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const isUnsupportedMethod = jvmFailureReason.includes("UNSUPPORTEDMETHOD");
|
const isUnsupportedMethod = jvmFailureReason.includes("UNSUPPORTEDMETHOD");
|
||||||
if (backendMode === "jvm" && !isUnsupportedMethod) {
|
if (backendMode === "jvm" && !isUnsupportedMethod) {
|
||||||
throw new Error(jvmFailureReason);
|
throw new Error(jvmFailureReason);
|
||||||
@ -1202,6 +1225,8 @@ async function runExternalExtract(
|
|||||||
} else {
|
} else {
|
||||||
logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`);
|
logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user