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,38 +1169,63 @@ 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 jvmResult = await runJvmExtractCommand(
|
const maxJvmAttempts = hybridMode ? 2 : 1;
|
||||||
layout,
|
for (let jvmAttempt = 1; jvmAttempt <= maxJvmAttempts; jvmAttempt++) {
|
||||||
archivePath,
|
const jvmResult = await runJvmExtractCommand(
|
||||||
targetDir,
|
layout,
|
||||||
conflictMode,
|
archivePath,
|
||||||
passwordCandidates,
|
targetDir,
|
||||||
onArchiveProgress,
|
conflictMode,
|
||||||
signal,
|
passwordCandidates,
|
||||||
timeoutMs
|
onArchiveProgress,
|
||||||
);
|
signal,
|
||||||
|
timeoutMs
|
||||||
|
);
|
||||||
|
|
||||||
if (jvmResult.ok) {
|
if (jvmResult.ok) {
|
||||||
logger.info(`Entpackt via ${jvmResult.backend || "jvm"} [CPU=Idle, I/O=Normal, single-thread]: ${path.basename(archivePath)}`);
|
if (jvmAttempt > 1) {
|
||||||
return jvmResult.usedPassword;
|
logger.info(`JVM-Extractor Retry #${jvmAttempt - 1} erfolgreich: ${path.basename(archivePath)}`);
|
||||||
}
|
}
|
||||||
if (jvmResult.aborted) {
|
logger.info(`Entpackt via ${jvmResult.backend || "jvm"} [CPU=Idle, I/O=Normal, single-thread]: ${path.basename(archivePath)}`);
|
||||||
throw new Error("aborted:extract");
|
return jvmResult.usedPassword;
|
||||||
}
|
}
|
||||||
if (jvmResult.timedOut) {
|
if (jvmResult.aborted) {
|
||||||
throw new Error(jvmResult.errorText || `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s`);
|
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";
|
jvmFailureReason = jvmResult.errorText || "JVM-Extractor fehlgeschlagen";
|
||||||
const isUnsupportedMethod = jvmFailureReason.includes("UNSUPPORTEDMETHOD");
|
|
||||||
if (backendMode === "jvm" && !isUnsupportedMethod) {
|
// In hybrid mode, retry once on "codecs" / "can't be opened" errors —
|
||||||
throw new Error(jvmFailureReason);
|
// 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 (isUnsupportedMethod) {
|
if (hybridMode && isTransientOpen && jvmAttempt < maxJvmAttempts) {
|
||||||
logger.warn(`JVM-Extractor: Komprimierungsmethode nicht unterstützt, fallback auf Legacy: ${path.basename(archivePath)}`);
|
logger.warn(`JVM-Extractor Hybrid-Retry: ${jvmFailureReason} — warte 3s vor Versuch #${jvmAttempt + 1}: ${path.basename(archivePath)}`);
|
||||||
} else {
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
logger.warn(`JVM-Extractor Fehler, fallback auf Legacy: ${jvmFailureReason}`);
|
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