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:
Sucukdeluxe 2026-03-03 16:35:43 +01:00
parent f9b0bbe676
commit 353cef7dbd
11 changed files with 57 additions and 92 deletions

View File

@ -11,7 +11,6 @@ import net.sf.sevenzipjbinding.IInStream;
import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.ICryptoGetTextPassword;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.ArchiveFormat;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
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 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 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 JBindExtractorMain() {
@ -330,78 +327,21 @@ public final class JBindExtractorMain {
SevenZipVolumeCallback callback = new SevenZipVolumeCallback(archiveFile, effectivePassword);
// 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()) {
VolumedArchiveInStream volumed = new VolumedArchiveInStream(archiveFile.getName(), callback);
IInArchive archive = SevenZip.openInArchive(null, volumed, callback);
return new SevenZipArchiveContext(archive, null, volumed, callback);
}
// Multi-part RAR (.part1.rar, .part2.rar or old-style .rar/.r01/.r02):
// The first stream MUST be obtained via the callback so the volume name
// tracker is properly initialized. 7z-JBinding uses getProperty(NAME)
// 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
// All other archives (including multi-part RAR): use RandomAccessFileInStream
// with auto-detection. The IArchiveOpenVolumeCallback handles additional
// volumes when 7z-JBinding requests them.
RandomAccessFile raf = new RandomAccessFile(archiveFile, "r");
RandomAccessFileInStream stream = new RandomAccessFileInStream(raf);
IInArchive archive = SevenZip.openInArchive(null, stream, 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) {
if (error == null) {
return false;

View File

@ -1169,7 +1169,17 @@ async function runExternalExtract(
}
logger.warn(`JVM-Extractor nicht verfügbar, nutze Legacy-Extractor: ${path.basename(archivePath)}`);
} 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)}`);
const maxJvmAttempts = hybridMode ? 2 : 1;
for (let jvmAttempt = 1; jvmAttempt <= maxJvmAttempts; jvmAttempt++) {
const jvmResult = await runJvmExtractCommand(
layout,
archivePath,
@ -1182,6 +1192,9 @@ async function runExternalExtract(
);
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)}`);
return jvmResult.usedPassword;
}
@ -1193,6 +1206,16 @@ async function runExternalExtract(
}
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");
if (backendMode === "jvm" && !isUnsupportedMethod) {
throw new Error(jvmFailureReason);
@ -1202,6 +1225,8 @@ async function runExternalExtract(
} else {
logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`);
}
break;
}
}
}