diff --git a/src/main/java/org/terasology/launcher/game/GameManager.java b/src/main/java/org/terasology/launcher/game/GameManager.java index 451cd128..d6b236d0 100644 --- a/src/main/java/org/terasology/launcher/game/GameManager.java +++ b/src/main/java/org/terasology/launcher/game/GameManager.java @@ -10,18 +10,17 @@ import org.slf4j.LoggerFactory; import org.terasology.launcher.model.GameIdentifier; import org.terasology.launcher.model.GameRelease; +import org.terasology.launcher.remote.DownloadException; +import org.terasology.launcher.remote.DownloadUtils; +import org.terasology.launcher.remote.RemoteResource; import org.terasology.launcher.tasks.ProgressListener; -import org.terasology.launcher.util.DownloadException; -import org.terasology.launcher.util.DownloadUtils; import org.terasology.launcher.util.FileUtils; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.Comparator; import java.util.Objects; import java.util.Set; @@ -52,17 +51,6 @@ public GameManager(Path cacheDirectory, Path installDirectory) { scanInstallationDir(); } - /** - * Derive the file name for the downloaded ZIP package from the game release. - */ - private String getFileNameFor(GameRelease release) { - GameIdentifier id = release.getId(); - String profileString = id.getProfile().toString().toLowerCase(); - String versionString = id.getDisplayVersion(); - String buildString = id.getBuild().toString().toLowerCase(); - return "terasology-" + profileString + "-" + versionString + "-" + buildString + ".zip"; - } - /** * Installs the given release to the local file system. * @@ -70,7 +58,7 @@ private String getFileNameFor(GameRelease release) { * @param listener the object which is to be informed about task progress */ public void install(GameRelease release, ProgressListener listener) throws IOException, DownloadException, InterruptedException { - final Path cachedZip = cacheDirectory.resolve(getFileNameFor(release)); + final Path cachedZip = cacheDirectory.resolve(release.getFilename()); // TODO: Properly validate cache and handle exceptions if (Files.notExists(cachedZip)) { @@ -85,30 +73,18 @@ public void install(GameRelease release, ProgressListener listener) throws IOExc } } + /** + * @deprecated Use {@link DownloadUtils#download(RemoteResource, Path, ProgressListener)} instead. + */ + @Deprecated private void download(GameRelease release, Path targetLocation, ProgressListener listener) throws DownloadException, IOException, InterruptedException { - final URL downloadUrl = release.getUrl(); - - final long contentLength = DownloadUtils.getContentLength(downloadUrl); - final long availableSpace = targetLocation.getParent().toFile().getUsableSpace(); - - if (availableSpace >= contentLength) { - final Path cacheZipPart = targetLocation.resolveSibling(targetLocation.getFileName().toString() + ".part"); - Files.deleteIfExists(cacheZipPart); - try { - DownloadUtils.downloadToFile(downloadUrl, cacheZipPart, listener).get(); - } catch (ExecutionException e) { - throw new DownloadException("Exception while downloading " + downloadUrl, e.getCause()); - } - - if (!listener.isCancelled()) { - Files.move(cacheZipPart, targetLocation, StandardCopyOption.ATOMIC_MOVE); - } - } else { - throw new DownloadException("Insufficient space for downloading package"); + DownloadUtils downloader = new DownloadUtils(); + try { + downloader.download(release, targetLocation, listener).get(); + } catch (ExecutionException e) { + throw new DownloadException("Download failed.", e.getCause()); } - - logger.info("Finished downloading package: {}", release.getId()); } /** diff --git a/src/main/java/org/terasology/launcher/model/GameRelease.java b/src/main/java/org/terasology/launcher/model/GameRelease.java index 9a66e7d4..deb4e02c 100644 --- a/src/main/java/org/terasology/launcher/model/GameRelease.java +++ b/src/main/java/org/terasology/launcher/model/GameRelease.java @@ -3,6 +3,8 @@ package org.terasology.launcher.model; +import org.terasology.launcher.remote.RemoteResource; + import java.net.URL; import java.util.Date; import java.util.Objects; @@ -17,7 +19,7 @@ *
  • TODO: define what the artifact is, and what requirements/restrictions there are
  • * */ -public class GameRelease { +public class GameRelease implements RemoteResource { final GameIdentifier id; final ReleaseMetadata releaseMetadata; final URL url; @@ -36,6 +38,19 @@ public URL getUrl() { return url; } + @Override + public String getFilename() { + String profileString = id.getProfile().toString().toLowerCase(); + String versionString = id.getDisplayVersion(); + String buildString = id.getBuild().toString().toLowerCase(); + return "terasology-" + profileString + "-" + versionString + "-" + buildString + ".zip"; + } + + @Override + public GameIdentifier getInfo() { + return id; + } + /** * The changelog associated with the game release */ diff --git a/src/main/java/org/terasology/launcher/util/DownloadException.java b/src/main/java/org/terasology/launcher/remote/DownloadException.java similarity index 85% rename from src/main/java/org/terasology/launcher/util/DownloadException.java rename to src/main/java/org/terasology/launcher/remote/DownloadException.java index 43961be1..77eb6803 100644 --- a/src/main/java/org/terasology/launcher/util/DownloadException.java +++ b/src/main/java/org/terasology/launcher/remote/DownloadException.java @@ -1,7 +1,7 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2023 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -package org.terasology.launcher.util; +package org.terasology.launcher.remote; public final class DownloadException extends RuntimeException { diff --git a/src/main/java/org/terasology/launcher/util/DownloadUtils.java b/src/main/java/org/terasology/launcher/remote/DownloadUtils.java similarity index 64% rename from src/main/java/org/terasology/launcher/util/DownloadUtils.java rename to src/main/java/org/terasology/launcher/remote/DownloadUtils.java index 0d711ebc..9beb28a0 100644 --- a/src/main/java/org/terasology/launcher/util/DownloadUtils.java +++ b/src/main/java/org/terasology/launcher/remote/DownloadUtils.java @@ -1,17 +1,17 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2023 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 -package org.terasology.launcher.util; +package org.terasology.launcher.remote; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.launcher.tasks.ProgressListener; +import javax.net.ssl.HttpsURLConnection; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.net.http.HttpClient; @@ -19,19 +19,63 @@ import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.time.Duration; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; public final class DownloadUtils { private static final Logger logger = LoggerFactory.getLogger(DownloadUtils.class); - private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(30); - private static final Duration READ_TIMEOUT = Duration.ofMinutes(5); + private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(30); + private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(5); - private DownloadUtils() { + private final Duration connectTimeout; //TODO: use instead of default + private final Duration readTimeout; //TODO: use instead of default + + public DownloadUtils() { + this(DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT); + } + + public DownloadUtils(Duration connectTimeout, Duration readTimeout) { + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; + } + + public CompletableFuture download(RemoteResource resource, Path path, ProgressListener listener) + throws DownloadException, IOException, InterruptedException { + final URL downloadUrl = resource.getUrl(); + + final long contentLength = DownloadUtils.getContentLength(downloadUrl); + final long availableSpace = path.getParent().toFile().getUsableSpace(); + + if (availableSpace >= contentLength) { + final Path cacheZipPart = path.resolveSibling(path.getFileName().toString() + ".part"); + Files.deleteIfExists(cacheZipPart); + try { + DownloadUtils.downloadToFile(downloadUrl, cacheZipPart, listener).get(); + } catch (ExecutionException e) { + throw new DownloadException("Exception while downloading " + downloadUrl, e.getCause()); + } + + if (!listener.isCancelled()) { + Files.move(cacheZipPart, path, StandardCopyOption.ATOMIC_MOVE); + } + } else { + throw new DownloadException("Insufficient space for downloading package"); + } + + logger.info("Finished downloading package: {}", resource.getInfo()); + + + return CompletableFuture.supplyAsync(() -> path); } + /** + * @deprecated Use {@link #download(RemoteResource, Path, ProgressListener)} instead; + */ + @Deprecated public static CompletableFuture downloadToFile(URL downloadURL, Path file, ProgressListener listener) throws DownloadException { listener.update(0); @@ -62,30 +106,27 @@ public static CompletableFuture downloadToFile(URL downloadURL, Path file, }); } + @Deprecated public static long getContentLength(URL downloadURL) throws DownloadException { - HttpURLConnection connection = null; + HttpsURLConnection connection = null; try { - connection = (HttpURLConnection) downloadURL.openConnection(); + connection = (HttpsURLConnection) downloadURL.openConnection(); connection.setRequestMethod("HEAD"); return connection.getContentLengthLong(); } catch (IOException e) { throw new DownloadException("Could not send HEAD request to HTTP-URL! URL=" + downloadURL, e); - } finally { - if (connection != null) { - connection.disconnect(); - } } } private static CompletableFuture> getConnectedDownloadConnection(URL downloadURL) throws DownloadException { var client = HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NORMAL) - .connectTimeout(CONNECT_TIMEOUT) + .connectTimeout(DEFAULT_CONNECT_TIMEOUT) .build(); HttpRequest request; try { - request = HttpRequest.newBuilder(downloadURL.toURI()).timeout(READ_TIMEOUT).build(); + request = HttpRequest.newBuilder(downloadURL.toURI()).timeout(DEFAULT_READ_TIMEOUT).build(); } catch (URISyntaxException e) { throw new DownloadException("Error in URL: " + downloadURL, e); } diff --git a/src/main/java/org/terasology/launcher/remote/RemoteResource.java b/src/main/java/org/terasology/launcher/remote/RemoteResource.java new file mode 100644 index 00000000..74f6d9c5 --- /dev/null +++ b/src/main/java/org/terasology/launcher/remote/RemoteResource.java @@ -0,0 +1,17 @@ +// Copyright 2023 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.launcher.remote; + +import java.net.URL; + +public interface RemoteResource { + + URL getUrl(); + + String getFilename(); + + T getInfo(); + + //TODO: String getChecksum(); +} diff --git a/src/main/java/org/terasology/launcher/tasks/DownloadTask.java b/src/main/java/org/terasology/launcher/tasks/DownloadTask.java index 8983e161..b01dc475 100644 --- a/src/main/java/org/terasology/launcher/tasks/DownloadTask.java +++ b/src/main/java/org/terasology/launcher/tasks/DownloadTask.java @@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory; import org.terasology.launcher.game.GameManager; import org.terasology.launcher.model.GameRelease; -import org.terasology.launcher.util.DownloadException; +import org.terasology.launcher.remote.DownloadException; import java.io.IOException;