Skip to content

Commit

Permalink
chore: introduce RemoteResource<T> interface (#712)
Browse files Browse the repository at this point in the history
* chore: move DownloadException and DownloadUtils to `remote` package

* feat: add `RemoteResource` interface

* feat: make GameRelease a RemoteResource

* chore: implement GameManager#download with DownloadUtils

* inline use of (deprecated) GameManager::getFileNameFor

* chore: remove unused import

---------

Co-authored-by: jdrueckert <jd.rueckert@googlemail.com>
  • Loading branch information
skaldarnar and jdrueckert committed Jun 22, 2024
1 parent 8237b1e commit 13f4108
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 55 deletions.
50 changes: 13 additions & 37 deletions src/main/java/org/terasology/launcher/game/GameManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,25 +51,14 @@ 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.
*
* @param release the game release to be installed
* @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)) {
Expand All @@ -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());
}

/**
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/org/terasology/launcher/model/GameRelease.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,7 +19,7 @@
* <li>TODO: define what the <b>artifact</b> is, and what requirements/restrictions there are</li>
* </ul>
*/
public class GameRelease {
public class GameRelease implements RemoteResource<GameIdentifier> {
final GameIdentifier id;
final ReleaseMetadata releaseMetadata;
final URL url;
Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,81 @@
// 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;
import java.net.http.HttpRequest;
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 <T> CompletableFuture<Path> download(RemoteResource<T> 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<Void> downloadToFile(URL downloadURL, Path file, ProgressListener listener) throws DownloadException {
listener.update(0);

Expand Down Expand Up @@ -62,30 +106,27 @@ public static CompletableFuture<Void> 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<HttpResponse<InputStream>> 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);
}
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/org/terasology/launcher/remote/RemoteResource.java
Original file line number Diff line number Diff line change
@@ -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<T> {

URL getUrl();

String getFilename();

T getInfo();

//TODO: String getChecksum();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 13f4108

Please sign in to comment.