Fix ZIP entry order: write .jsdos/ config first, then game files sorted
Build & Deploy / build-and-deploy (push) Successful in 40s

js-dos reads ZIP entries sequentially and needs .jsdos/dosbox.conf and
.jsdos/jsdos.json first to configure DOSBox. Java's Files.walk() uses
filesystem readdir order which put them at the very end of the ZIP
(after the 328MB CloneCD image), causing extraction failures.

Now:
1. First pass: walk .jsdos/ directory and write config files first
2. Second pass: walk everything else (excluding .jsdos/) in sorted order
3. All directory ancestors are still added as explicit entries
4. .sorted() ensures deterministic, alphabetical entry order
This commit is contained in:
Hermes Agent
2026-05-28 13:47:52 +02:00
parent 8f14ecb5a1
commit eb37cd43bb
@@ -290,27 +290,52 @@ public class UploadResource {
// ZIP it up directly from extractDir (no temp copy) // ZIP it up directly from extractDir (no temp copy)
try (var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(bundlePath))) { try (var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(bundlePath))) {
var addedDirs = new java.util.HashSet<String>(); var addedDirs = new java.util.HashSet<String>();
Files.walk(extractDir).filter(Files::isRegularFile).forEach(f -> {
try { // Helper: write a directory entry and all its ancestors if not yet added
String entryName = extractDir.relativize(f).toString().replace('\\', '/'); // (Emscripten FS requires every directory in the chain as explicit entries)
// Ensure ALL ancestor directories have explicit entries java.util.function.Consumer<String> ensureDir = entryName -> {
// (Emscripten FS requires every directory in the chain)
int idx = entryName.lastIndexOf('/'); int idx = entryName.lastIndexOf('/');
while (idx >= 0) { while (idx >= 0) {
String parent = entryName.substring(0, idx + 1); String parent = entryName.substring(0, idx + 1);
if (addedDirs.add(parent)) { if (addedDirs.add(parent)) {
try {
zos.putNextEntry(new java.util.zip.ZipEntry(parent)); zos.putNextEntry(new java.util.zip.ZipEntry(parent));
zos.closeEntry(); zos.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} }
idx = entryName.lastIndexOf('/', idx - 1); idx = entryName.lastIndexOf('/', idx - 1);
} }
};
// Helper: write a single file entry with its directory chain
java.util.function.Consumer<Path> writeFile = f -> {
try {
String entryName = extractDir.relativize(f).toString().replace('\\', '/');
ensureDir.accept(entryName);
zos.putNextEntry(new java.util.zip.ZipEntry(entryName)); zos.putNextEntry(new java.util.zip.ZipEntry(entryName));
Files.copy(f, zos); Files.copy(f, zos);
zos.closeEntry(); zos.closeEntry();
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }
}); };
// ── First pass: .jsdos config files (must come first in ZIP) ──
try (var walk = Files.walk(jsdos)) {
walk.filter(Files::isRegularFile)
.sorted() // deterministic order
.forEach(writeFile);
}
// ── Second pass: all other game files ──
try (var walk = Files.walk(extractDir)) {
walk.filter(Files::isRegularFile)
.filter(f -> !f.startsWith(jsdos))
.sorted() // deterministic order
.forEach(writeFile);
}
} }
// Clean up .jsdos config files from the extract dir // Clean up .jsdos config files from the extract dir