diff --git a/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java b/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java index 25661f8..a8358f2 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java +++ b/backend/src/main/java/de/grimsi/gameyfin/rest/GamesController.java @@ -5,11 +5,16 @@ import de.grimsi.gameyfin.entities.DetectedGame; import de.grimsi.gameyfin.service.DownloadService; import de.grimsi.gameyfin.service.GameService; import lombok.RequiredArgsConstructor; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Map; @@ -50,10 +55,21 @@ public class GamesController { DetectedGame game = gameService.getDetectedGame(slug); String downloadFileName = downloadService.getDownloadFileName(game); + long downloadFileSize = downloadService.getDownloadFileSize(game); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"%s\"".formatted(downloadFileName)); + headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + headers.add(HttpHeaders.PRAGMA, "no-cache"); + headers.add(HttpHeaders.EXPIRES, "0"); + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); + if (downloadFileSize > 0) { + headers.setContentLength(downloadFileSize); + } return ResponseEntity .ok() - .header("Content-Disposition", "attachment; filename=\"%s\"".formatted(downloadFileName)) + .headers(headers) .body(out -> downloadService.sendGamefilesToClient(game, out)); } diff --git a/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java b/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java index 563e522..c29fe20 100644 --- a/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java +++ b/backend/src/main/java/de/grimsi/gameyfin/service/DownloadService.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; +import org.apache.commons.io.FileUtils; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; @@ -34,6 +35,23 @@ public class DownloadService { return getFilenameWithExtension(path) + ".zip"; } + public long getDownloadFileSize(DetectedGame game) { + Path path = Path.of(game.getPath()); + + try { + if (!path.toFile().isDirectory()) { + long fileSize = filesystemService.getSizeOnDisk(path); + log.info("Calculated file size for {} ({} MB).", path, Math.divideExact(fileSize, 1000000L)); + return fileSize; + } else { + // return zero since we cannot set content length for ZipOutputStreams that are used to archive directories + return 0; + } + } catch (IOException e) { + throw new DownloadAbortedException(); + } + } + public Resource sendImageToClient(String imageId) { String filename = "%s.png".formatted(imageId); return filesystemService.getFileFromCache(filename); @@ -78,6 +96,7 @@ public class DownloadService { } private void sendGamefilesAsZipToClient(Path path, OutputStream outputStream) { + log.info("Archiving game path {} for download...", path); ZipOutputStream zos = new ZipOutputStream(outputStream) {{ def.setLevel(Deflater.NO_COMPRESSION); }}; @@ -87,6 +106,7 @@ public class DownloadService { @SneakyThrows public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { zos.putNextEntry(new ZipEntry(path.relativize(file).toString())); + log.debug("Adding file {} to archive...", file); Files.copy(file, zos); zos.closeEntry(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ef089f2..0c909b5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "1.2.3-SNAPSHOT", + "version": "1.2.4-SNAPSHOT", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "frontend", - "version": "1.2.3-SNAPSHOT", + "version": "1.2.4-SNAPSHOT", "dependencies": { "@angular/animations": "^14.0.0", "@angular/cdk": "^14.1.0", diff --git a/frontend/package.json b/frontend/package.json index cf5b2db..6734fcf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "1.2.3-SNAPSHOT", + "version": "1.2.4-SNAPSHOT", "scripts": { "ng": "ng", "start": "ng serve",