From 7d9a1bc3fc1f65a46c6b00ba8adbf8dc4250a44f Mon Sep 17 00:00:00 2001 From: Riley Park Date: Sat, 28 May 2022 15:52:58 -0700 Subject: [PATCH] feat: add support for providing download from a CDN instead of local storage --- .../configuration/AppConfiguration.java | 12 +++ .../controller/v2/DownloadController.java | 77 ++++++++++++++++--- .../v2/VersionFamilyBuildsController.java | 2 +- .../java/io/papermc/bibliothek/util/HTTP.java | 5 +- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/main/java/io/papermc/bibliothek/configuration/AppConfiguration.java b/src/main/java/io/papermc/bibliothek/configuration/AppConfiguration.java index 1f1f5c1..866b146 100644 --- a/src/main/java/io/papermc/bibliothek/configuration/AppConfiguration.java +++ b/src/main/java/io/papermc/bibliothek/configuration/AppConfiguration.java @@ -26,6 +26,7 @@ import jakarta.validation.constraints.NotNull; import java.net.URL; import java.nio.file.Path; +import org.jetbrains.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.validation.annotation.Validated; @@ -35,6 +36,7 @@ public class AppConfiguration { private URL apiBaseUrl; private String apiTitle; private @NotNull Path storagePath; + private String cdnUrl; @SuppressWarnings("checkstyle:MethodName") public URL getApiBaseUrl() { @@ -65,4 +67,14 @@ public Path getStoragePath() { public void setStoragePath(final Path storagePath) { this.storagePath = storagePath; } + + @SuppressWarnings("checkstyle:MethodName") + public @Nullable String getCdnUrl() { + return this.cdnUrl; + } + + @SuppressWarnings("checkstyle:MethodName") + public void setCdnUrl(final String cdnUrl) { + this.cdnUrl = cdnUrl; + } } diff --git a/src/main/java/io/papermc/bibliothek/controller/v2/DownloadController.java b/src/main/java/io/papermc/bibliothek/controller/v2/DownloadController.java index ebfcddd..d73f7f0 100644 --- a/src/main/java/io/papermc/bibliothek/controller/v2/DownloadController.java +++ b/src/main/java/io/papermc/bibliothek/controller/v2/DownloadController.java @@ -43,12 +43,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.validation.constraints.Pattern; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.Map; +import java.util.function.BiFunction; +import org.jetbrains.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.AbstractResource; import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.UrlResource; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -135,12 +140,15 @@ public ResponseEntity download( for (final Map.Entry download : build.downloads().entrySet()) { if (download.getValue().name().equals(downloadName)) { try { - return new JavaArchive( - this.configuration.getStoragePath() + return JavaArchive.resolve( + this.configuration, + download.getValue(), + (cdn, file) -> URI.create(String.format("%s/%s/%s/%d/%s", cdn, project.name(), version.name(), build.number(), file.name())), + (path, file) -> path .resolve(project.name()) .resolve(version.name()) .resolve(String.valueOf(build.number())) - .resolve(download.getValue().name()), + .resolve(file.name()), CACHE ); } catch (final IOException e) { @@ -151,17 +159,68 @@ public ResponseEntity download( throw new DownloadNotFound(); } - private static class JavaArchive extends ResponseEntity { - JavaArchive(final Path path, final CacheControl cache) throws IOException { - super(new FileSystemResource(path), headersFor(path, cache), HttpStatus.OK); + private static class JavaArchive extends ResponseEntity { + public static JavaArchive resolve( + final AppConfiguration config, + final Build.Download download, + final BiFunction cdnGetter, + final BiFunction localGetter, + final CacheControl cache + ) throws IOException { + @Nullable IOException cdnException = null; + final @Nullable String cdnUrl = config.getCdnUrl(); + if (cdnUrl != null) { + final @Nullable URI cdn = cdnGetter.apply(cdnUrl, download); + if (cdn != null) { + try { + return forUrl(download, cdn, cache); + } catch (final IOException e) { + cdnException = e; + } + } + } + @Nullable IOException localException = null; + final @Nullable Path local = localGetter.apply(config.getStoragePath(), download); + if (local != null) { + try { + return forPath(download, local, cache); + } catch (final IOException e) { + localException = e; + } + } + final IOException exception = new IOException("Could not resolve download via CDN or Local Storage"); + if (cdnException != null) { + exception.addSuppressed(cdnException); + } + if (localException != null) { + exception.addSuppressed(localException); + } + throw exception; } - private static HttpHeaders headersFor(final Path path, final CacheControl cache) throws IOException { + private static JavaArchive forUrl(final Build.Download download, final URI uri, final CacheControl cache) throws IOException { + final UrlResource resource = new UrlResource(uri); + final HttpHeaders headers = headersFor(download, cache); + headers.setLastModified(resource.lastModified()); + return new JavaArchive(resource, headers); + } + + private static JavaArchive forPath(final Build.Download download, final Path path, final CacheControl cache) throws IOException { + final FileSystemResource resource = new FileSystemResource(path); + final HttpHeaders headers = headersFor(download, cache); + headers.setLastModified(Files.getLastModifiedTime(path).toInstant()); + return new JavaArchive(resource, headers); + } + + private JavaArchive(final AbstractResource resource, final HttpHeaders headers) { + super(resource, headers, HttpStatus.OK); + } + + private static HttpHeaders headersFor(final Build.Download download, final CacheControl cache) { final HttpHeaders headers = new HttpHeaders(); headers.setCacheControl(cache); - headers.setContentDisposition(HTTP.attachmentDisposition(path.getFileName())); + headers.setContentDisposition(HTTP.attachmentDisposition(download.name())); headers.setContentType(HTTP.APPLICATION_JAVA_ARCHIVE); - headers.setLastModified(Files.getLastModifiedTime(path).toInstant()); return headers; } } diff --git a/src/main/java/io/papermc/bibliothek/controller/v2/VersionFamilyBuildsController.java b/src/main/java/io/papermc/bibliothek/controller/v2/VersionFamilyBuildsController.java index c1d5733..ce8612f 100644 --- a/src/main/java/io/papermc/bibliothek/controller/v2/VersionFamilyBuildsController.java +++ b/src/main/java/io/papermc/bibliothek/controller/v2/VersionFamilyBuildsController.java @@ -137,7 +137,7 @@ static VersionFamilyBuildsResponse from(final Project project, final VersionFami } @Schema - public static record VersionFamilyBuild( + public record VersionFamilyBuild( @Schema(name = "version", pattern = Version.PATTERN, example = "1.18") String version, @Schema(name = "build", pattern = "\\d+", example = "10") diff --git a/src/main/java/io/papermc/bibliothek/util/HTTP.java b/src/main/java/io/papermc/bibliothek/util/HTTP.java index de7ec76..887bb00 100644 --- a/src/main/java/io/papermc/bibliothek/util/HTTP.java +++ b/src/main/java/io/papermc/bibliothek/util/HTTP.java @@ -24,7 +24,6 @@ package io.papermc.bibliothek.util; import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import java.time.Duration; import org.springframework.http.CacheControl; import org.springframework.http.ContentDisposition; @@ -48,7 +47,7 @@ public static CacheControl sMaxAgePublicCache(final Duration sMaxAge) { .sMaxAge(sMaxAge); } - public static ContentDisposition attachmentDisposition(final Path filename) { - return ContentDisposition.attachment().filename(filename.getFileName().toString(), StandardCharsets.UTF_8).build(); + public static ContentDisposition attachmentDisposition(final String filename) { + return ContentDisposition.attachment().filename(filename, StandardCharsets.UTF_8).build(); } }