Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: introduce RemoteResource<T> interface #712

Merged
merged 9 commits into from
Jun 22, 2024
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

am lacking experience here, but the method is private. why deprecating it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether it actually has any effect other than your IDE telling you this is deprecated 🤷 I kinda see it as a TODO to replace the call side of this method with what is written in the deprecation note.
That way, I don't change any interfaces now, and can make this change in a follow up PR to keep the PRs smaller.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a deprecation note to the upper one, too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was only a single use of that other method, so I just inlined it and removed the method in 482ba65.

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
Loading