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.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;
|
||||
|
||||
@ -1169,38 +1169,63 @@ 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 jvmResult = await runJvmExtractCommand(
|
||||
layout,
|
||||
archivePath,
|
||||
targetDir,
|
||||
conflictMode,
|
||||
passwordCandidates,
|
||||
onArchiveProgress,
|
||||
signal,
|
||||
timeoutMs
|
||||
);
|
||||
const maxJvmAttempts = hybridMode ? 2 : 1;
|
||||
for (let jvmAttempt = 1; jvmAttempt <= maxJvmAttempts; jvmAttempt++) {
|
||||
const jvmResult = await runJvmExtractCommand(
|
||||
layout,
|
||||
archivePath,
|
||||
targetDir,
|
||||
conflictMode,
|
||||
passwordCandidates,
|
||||
onArchiveProgress,
|
||||
signal,
|
||||
timeoutMs
|
||||
);
|
||||
|
||||
if (jvmResult.ok) {
|
||||
logger.info(`Entpackt via ${jvmResult.backend || "jvm"} [CPU=Idle, I/O=Normal, single-thread]: ${path.basename(archivePath)}`);
|
||||
return jvmResult.usedPassword;
|
||||
}
|
||||
if (jvmResult.aborted) {
|
||||
throw new Error("aborted:extract");
|
||||
}
|
||||
if (jvmResult.timedOut) {
|
||||
throw new Error(jvmResult.errorText || `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s`);
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (jvmResult.aborted) {
|
||||
throw new Error("aborted:extract");
|
||||
}
|
||||
if (jvmResult.timedOut) {
|
||||
throw new Error(jvmResult.errorText || `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s`);
|
||||
}
|
||||
|
||||
jvmFailureReason = jvmResult.errorText || "JVM-Extractor fehlgeschlagen";
|
||||
const isUnsupportedMethod = jvmFailureReason.includes("UNSUPPORTEDMETHOD");
|
||||
if (backendMode === "jvm" && !isUnsupportedMethod) {
|
||||
throw new Error(jvmFailureReason);
|
||||
}
|
||||
if (isUnsupportedMethod) {
|
||||
logger.warn(`JVM-Extractor: Komprimierungsmethode nicht unterstützt, fallback auf Legacy: ${path.basename(archivePath)}`);
|
||||
} else {
|
||||
logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`);
|
||||
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);
|
||||
}
|
||||
if (isUnsupportedMethod) {
|
||||
logger.warn(`JVM-Extractor: Komprimierungsmethode nicht unterstützt, fallback auf Legacy: ${path.basename(archivePath)}`);
|
||||
} else {
|
||||
logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user