feat: stream setup .jsdos bundles on-the-fly instead of duplicating game data
Build & Deploy / build-and-deploy (push) Failing after 1m20s
Build & Deploy / build-and-deploy (push) Failing after 1m20s
- Add streamSetupBundle() to GameService — reads main bundle as zip,
replaces config files (.jsdos/dosbox.conf, .jsdos/jsdos.json) with
setup variants pointing to SETUP.EXE, streams directly to response
- Add GET /api/games/{id}/setup-bundle endpoint that calls the above
- Remove .setup.jsdos disk creation during upload — setupExe is still
detected and stored in game metadata, but no duplicate bundle is saved
- Update frontend api.js to point setupBundleUrl() to the new endpoint
- Fix pre-existing backslash escaping bug in UploadResource.java path
normalization (replace('\', '/') instead of replace('\\', '/'))
This commit is contained in:
@@ -112,5 +112,5 @@ export function bundleUrl(game) {
|
||||
/** Get game bundle URL for setup mode (runs SETUP.EXE instead) */
|
||||
export function setupBundleUrl(game) {
|
||||
if (!game) return null;
|
||||
return `/games/${game.id}.setup.jsdos`;
|
||||
return `/api/games/${game.id}/setup-bundle`;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,34 @@ public class GameResource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a setup .jsdos bundle on-the-fly. No disk duplication —
|
||||
* reads the main bundle and patches config files to point at the
|
||||
* setup executable (SETUP.EXE, INSTALL.EXE, etc.).
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}/setup-bundle")
|
||||
@Produces("application/zip")
|
||||
public Response setupBundle(@PathParam("id") String id) {
|
||||
try {
|
||||
var game = svc.load(id);
|
||||
if (game.getSetupExe() == null || game.getSetupExe().isBlank()) {
|
||||
return Response.status(404)
|
||||
.entity(Map.of("error", "No setup executable configured for this game"))
|
||||
.build();
|
||||
}
|
||||
String filename = (game.getTitle() != null ? game.getTitle() : id) + ".setup.jsdos";
|
||||
StreamingOutput stream = output -> svc.streamSetupBundle(id, output);
|
||||
return Response.ok(stream)
|
||||
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
|
||||
.build();
|
||||
} catch (NoSuchFileException e) {
|
||||
return Response.status(404).entity(Map.of("error", "Game not found")).build();
|
||||
} catch (Exception e) {
|
||||
return Response.serverError().entity(Map.of("error", e.getMessage())).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}/download")
|
||||
@Produces("application/zip")
|
||||
|
||||
@@ -10,6 +10,7 @@ import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Instant;
|
||||
@@ -282,5 +283,45 @@ public class GameService {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a setup .jsdos bundle on-the-fly by reading the main bundle
|
||||
* and replacing config files to point at the setup executable.
|
||||
* No data is written to disk — the modified ZIP is streamed directly.
|
||||
*/
|
||||
public void streamSetupBundle(String id, OutputStream out) throws IOException {
|
||||
Game game = load(id);
|
||||
String setupExe = game.getSetupExe();
|
||||
if (setupExe == null || setupExe.isBlank()) {
|
||||
throw new IOException("No setup executable configured for this game");
|
||||
}
|
||||
|
||||
Path bundlePath = gamesDir.resolve(game.getBundleFile());
|
||||
if (!Files.exists(bundlePath)) {
|
||||
throw new NoSuchFileException("Bundle not found: " + game.getBundleFile());
|
||||
}
|
||||
|
||||
byte[] newDosboxConf = buildDosboxConfBytes(setupExe);
|
||||
byte[] newJsdosJson = buildJsdosJsonBytes(setupExe);
|
||||
|
||||
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath));
|
||||
var zos = new java.util.zip.ZipOutputStream(out)) {
|
||||
|
||||
java.util.zip.ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
String name = entry.getName();
|
||||
zos.putNextEntry(new java.util.zip.ZipEntry(name));
|
||||
if (".jsdos/dosbox.conf".equals(name)) {
|
||||
zos.write(newDosboxConf);
|
||||
} else if (".jsdos/jsdos.json".equals(name)) {
|
||||
zos.write(newJsdosJson);
|
||||
} else {
|
||||
zis.transferTo(zos);
|
||||
}
|
||||
zos.closeEntry();
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Path getGamesDir() { return gamesDir; }
|
||||
}
|
||||
|
||||
@@ -103,30 +103,25 @@ public class UploadResource {
|
||||
// Detect platform (DOS vs Windows) by reading the main EXE header
|
||||
String platform = detectPlatform(Path.of(mainExe));
|
||||
|
||||
// Create setup bundle only if the setup executable is itself DOS-compatible.
|
||||
// Store setup executable path if found and DOS-compatible.
|
||||
// Some games (e.g. Fallout) ship a Windows SETUP.EXE even though the main
|
||||
// game is DOS — that setup would fail with "This program cannot run in DOS mode".
|
||||
boolean hasSetupBundle = false;
|
||||
if (setupExe != null) {
|
||||
String setupPlatform = detectPlatform(Path.of(setupExe));
|
||||
if (!"windows".equals(setupPlatform)) {
|
||||
Path setupBundlePath = svc.getGamesDir().resolve(gameId + ".setup.jsdos");
|
||||
createBundle(extractDir, setupExe, setupBundlePath);
|
||||
hasSetupBundle = true;
|
||||
}
|
||||
}
|
||||
// The .setup.jsdos bundle is now generated on-the-fly — no disk duplication needed.
|
||||
|
||||
// Save metadata
|
||||
Game game = new Game(gameId, title);
|
||||
game.setBundleFile(bundleFile);
|
||||
game.setPlatform(platform);
|
||||
// Store the selected executable and the full list for the UI picker
|
||||
String relMain = extractDir.relativize(Path.of(mainExe)).toString().replace('\\', '/');
|
||||
String relMain = extractDir.relativize(Path.of(mainExe)).toString().replace('\', '/');
|
||||
game.setExecutable(relMain);
|
||||
game.setExecutables(findAllExecutables(extractDir));
|
||||
if (hasSetupBundle) {
|
||||
if (setupExe != null) {
|
||||
String setupPlatform = detectPlatform(Path.of(setupExe));
|
||||
if (!"windows".equals(setupPlatform)) {
|
||||
game.setSetupExe(extractDir.relativize(Path.of(setupExe)).toString());
|
||||
}
|
||||
}
|
||||
game.setReady(true);
|
||||
svc.save(game);
|
||||
|
||||
@@ -254,7 +249,7 @@ public class UploadResource {
|
||||
public FileVisitResult visitFile(Path f, BasicFileAttributes a) {
|
||||
String name = f.getFileName().toString().toLowerCase();
|
||||
if (name.endsWith(".exe") || name.endsWith(".com") || name.endsWith(".bat")) {
|
||||
result.add(dir.relativize(f).toString().replace('\\', '/'));
|
||||
result.add(dir.relativize(f).toString().replace('\', '/'));
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
@@ -433,7 +428,7 @@ public class UploadResource {
|
||||
var allDirs = new java.util.TreeSet<String>();
|
||||
try (var walk = Files.walk(extractDir)) {
|
||||
walk.filter(Files::isRegularFile).forEach(f -> {
|
||||
String entryName = extractDir.relativize(f).toString().replace('\\', '/');
|
||||
String entryName = extractDir.relativize(f).toString().replace('\', '/');
|
||||
int idx = entryName.lastIndexOf('/');
|
||||
while (idx >= 0) {
|
||||
allDirs.add(entryName.substring(0, idx + 1));
|
||||
@@ -454,7 +449,7 @@ public class UploadResource {
|
||||
.sorted()
|
||||
.forEach(f -> {
|
||||
try {
|
||||
String entryName = extractDir.relativize(f).toString().replace('\\', '/');
|
||||
String entryName = extractDir.relativize(f).toString().replace('\', '/');
|
||||
zos.putNextEntry(new java.util.zip.ZipEntry(entryName));
|
||||
Files.copy(f, zos);
|
||||
zos.closeEntry();
|
||||
@@ -476,7 +471,7 @@ public class UploadResource {
|
||||
.sorted()
|
||||
.forEach(f -> {
|
||||
try {
|
||||
String entryName = extractDir.relativize(f).toString().replace('\\', '/');
|
||||
String entryName = extractDir.relativize(f).toString().replace('\', '/');
|
||||
zos.putNextEntry(new java.util.zip.ZipEntry(entryName));
|
||||
Files.copy(f, zos);
|
||||
zos.closeEntry();
|
||||
@@ -501,7 +496,7 @@ public class UploadResource {
|
||||
* If relExe is just a filename, returns {null, relExe}.
|
||||
*/
|
||||
private static String[] splitDirExe(String relExe) {
|
||||
int idx = relExe.replace('\\', '/').lastIndexOf('/');
|
||||
int idx = relExe.replace('\', '/').lastIndexOf('/');
|
||||
if (idx >= 0) {
|
||||
return new String[]{relExe.substring(0, idx), relExe.substring(idx + 1)};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user