From 090591cdb440994b4b5cc754f9cad8df7fece9ae Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Sat, 11 Nov 2023 19:58:43 +0100 Subject: [PATCH] GH-1977 Enable cache headers on immutable repositories (Resolve #1977) --- .../kotlin/com/reposilite/ReposiliteRunner.kt | 6 ++++- .../reposilite/maven/MavenIntegrationTest.kt | 5 +++- .../maven/MavenMirrorsIntegrationTest.kt | 2 +- .../com/reposilite/maven/MavenFacade.kt | 5 ++-- .../kotlin/com/reposilite/maven/Repository.kt | 3 +++ .../com/reposilite/maven/RepositoryService.kt | 15 ++++++++--- .../com/reposilite/maven/api/LookupApi.kt | 6 +++++ .../maven/infrastructure/MavenEndpoints.kt | 11 +++++++- .../infrastructure/MavenLatestApiEndpoints.kt | 15 ++++++++--- .../shared/extensions/JavalinExtensions.kt | 25 ++++++++----------- .../web/application/JavalinConfiguration.kt | 4 +-- ...assHandler.kt => ApiCacheBypassHandler.kt} | 2 +- .../com/reposilite/maven/MavenFacadeTest.kt | 4 +-- 13 files changed, 70 insertions(+), 33 deletions(-) rename reposilite-backend/src/main/kotlin/com/reposilite/web/infrastructure/{CacheBypassHandler.kt => ApiCacheBypassHandler.kt} (94%) diff --git a/reposilite-backend/src/integration/kotlin/com/reposilite/ReposiliteRunner.kt b/reposilite-backend/src/integration/kotlin/com/reposilite/ReposiliteRunner.kt index 63fccd91e..af23b464e 100644 --- a/reposilite-backend/src/integration/kotlin/com/reposilite/ReposiliteRunner.kt +++ b/reposilite-backend/src/integration/kotlin/com/reposilite/ReposiliteRunner.kt @@ -116,7 +116,11 @@ internal abstract class ReposiliteRunner { redeployment = true, storageProvider = _storageProvider!!, ) - }.values.toList() + } + .toMutableMap() + .also { it["immutable"] = RepositorySettings(id = "immutable", redeployment = false) } + .values + .toList() ) } diff --git a/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenIntegrationTest.kt b/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenIntegrationTest.kt index 9df7efd4d..119a85937 100644 --- a/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenIntegrationTest.kt +++ b/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenIntegrationTest.kt @@ -23,6 +23,7 @@ import com.reposilite.maven.specification.MavenIntegrationSpecification import com.reposilite.shared.ErrorResponse import com.reposilite.RecommendedLocalSpecificationJunitExtension import com.reposilite.RecommendedRemoteSpecificationJunitExtension +import com.reposilite.shared.extensions.maxAge import com.reposilite.storage.api.DocumentInfo import io.javalin.http.HttpStatus.NOT_FOUND import io.javalin.http.HttpStatus.UNAUTHORIZED @@ -36,6 +37,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.util.concurrent.CompletableFuture import java.util.concurrent.CountDownLatch +import kong.unirest.HeaderNames.CACHE_CONTROL @ExtendWith(RecommendedLocalSpecificationJunitExtension::class) internal class LocalMavenIntegrationTest : MavenIntegrationTest() @@ -48,7 +50,7 @@ internal abstract class MavenIntegrationTest : MavenIntegrationSpecification() { @Test fun `should support head requests`() { // given: the details about an existing in repository file - val (repository, gav, file, content) = useDocument("releases", "gav", "artifact.jar", "content", true) + val (repository, gav, file, content) = useDocument("immutable", "gav", "artifact.jar", "content", true) // when: client requests head data val response = head("$base/$repository/$gav/$file").asEmpty() @@ -56,6 +58,7 @@ internal abstract class MavenIntegrationTest : MavenIntegrationSpecification() { // then: service returns valid file metadata assertThat(response.isSuccess).isTrue assertThat(response.headers.getFirst(CONTENT_LENGTH).toInt()).isEqualTo(content.length) + assertThat(response.headers.getFirst(CACHE_CONTROL)).isEqualTo("public, max-age=$maxAge") } @Test diff --git a/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenMirrorsIntegrationTest.kt b/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenMirrorsIntegrationTest.kt index bef7d9508..bb2b477c0 100644 --- a/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenMirrorsIntegrationTest.kt +++ b/reposilite-backend/src/integration/kotlin/com/reposilite/maven/MavenMirrorsIntegrationTest.kt @@ -74,7 +74,7 @@ internal abstract class MavenMirrorsIntegrationTest : MavenIntegrationSpecificat ) ) assertThat(localFile.isOk).isTrue - assertThat(localFile.get().second.readAllBytes().decodeToString()).isEqualTo("upstream") + assertThat(localFile.get().content.readAllBytes().decodeToString()).isEqualTo("upstream") } } diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt index b507352a2..2f01ab2db 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/MavenFacade.kt @@ -26,18 +26,17 @@ import com.reposilite.maven.api.LatestBadgeRequest import com.reposilite.maven.api.LatestVersionResponse import com.reposilite.maven.api.LookupRequest import com.reposilite.maven.api.Metadata +import com.reposilite.maven.api.ResolvedDocument import com.reposilite.maven.api.SaveMetadataRequest import com.reposilite.maven.api.VersionLookupRequest import com.reposilite.maven.api.VersionsResponse import com.reposilite.plugin.api.Facade import com.reposilite.shared.ErrorResponse import com.reposilite.storage.api.DirectoryInfo -import com.reposilite.storage.api.DocumentInfo import com.reposilite.storage.api.FileDetails import com.reposilite.storage.api.Location import com.reposilite.token.AccessTokenIdentifier import panda.std.Result -import java.io.InputStream class MavenFacade internal constructor( private val journalist: Journalist, @@ -52,7 +51,7 @@ class MavenFacade internal constructor( fun findDetails(lookupRequest: LookupRequest): Result = repositoryService.findDetails(lookupRequest) - fun findFile(lookupRequest: LookupRequest): Result, ErrorResponse> = + fun findFile(lookupRequest: LookupRequest): Result = repositoryService.findFile(lookupRequest) fun deployFile(deployRequest: DeployRequest): Result = diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/Repository.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/Repository.kt index 650cda4c4..e16840ee0 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/Repository.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/Repository.kt @@ -44,6 +44,9 @@ class Repository internal constructor( fun acceptsDeploymentOf(location: Location): Boolean = redeployment || location.getSimpleName().contains(METADATA_FILE) || !storageProvider.exists(location) + fun acceptsCachingOf(gav: Location): Boolean = + !redeployment && !gav.getSimpleName().contains(METADATA_FILE) + fun writeFileChecksums(location: Location, bytes: ByteArray): Result = writeFileChecksums(location, bytes.inputStream()) diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt index 30e1a8258..f29b7ac79 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/RepositoryService.kt @@ -22,8 +22,8 @@ import com.reposilite.maven.api.DeployEvent import com.reposilite.maven.api.DeployRequest import com.reposilite.maven.api.Identifier import com.reposilite.maven.api.LookupRequest -import com.reposilite.maven.api.METADATA_FILE import com.reposilite.maven.api.PreResolveEvent +import com.reposilite.maven.api.ResolvedDocument import com.reposilite.maven.api.ResolvedFileEvent import com.reposilite.plugin.Extensions import com.reposilite.shared.ErrorResponse @@ -110,8 +110,17 @@ internal class RepositoryService( fun findDetails(lookupRequest: LookupRequest): Result = resolve(lookupRequest) { repository, gav -> findDetails(lookupRequest.accessToken, repository, gav) } - fun findFile(lookupRequest: LookupRequest): Result, ErrorResponse> = - resolve(lookupRequest) { repository, gav -> findFile(lookupRequest.accessToken, repository, gav) } + fun findFile(lookupRequest: LookupRequest): Result = + resolve(lookupRequest) { repository, gav -> + findFile(lookupRequest.accessToken, repository, gav).map { + val (details, stream) = it + ResolvedDocument( + document = details, + cachable = repository.acceptsCachingOf(gav), + content = stream + ) + } + } private fun resolve(lookupRequest: LookupRequest, block: (Repository, Location) -> Result): Result { val (accessToken, repositoryName, gav) = lookupRequest diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/api/LookupApi.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/api/LookupApi.kt index e82ed9442..885f3cc07 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/api/LookupApi.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/api/LookupApi.kt @@ -55,3 +55,9 @@ data class ResolvedFileEvent( val gav: Location, var result: Result, ErrorResponse> ) : Event + +data class ResolvedDocument( + val document: DocumentInfo, + val content: InputStream, + val cachable: Boolean +) \ No newline at end of file diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenEndpoints.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenEndpoints.kt index 61eafac85..c1300ef1b 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenEndpoints.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenEndpoints.kt @@ -63,7 +63,16 @@ internal class MavenEndpoints( requireGav { gav -> LookupRequest(this?.identifier, requireParameter("repository"), gav) .let { request -> mavenFacade.findFile(request) } - .peek { (details, file) -> ctx.resultAttachment(details.name, details.contentType, details.contentLength, compressionStrategy, file) } + .peek { + ctx.resultAttachment( + name = it.document.name, + contentType = it.document.contentType, + contentLength = it.document.contentLength, + compressionStrategy = compressionStrategy, + cache = it.cachable, + data = it.content + ) + } .onError { ctx.status(it.status).html(frontendFacade.createNotFoundPage(uri, it.message)) mavenFacade.logger.debug("FIND | Could not find file due to $it") diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenLatestApiEndpoints.kt b/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenLatestApiEndpoints.kt index 51c6c7914..bc8d1a1f1 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenLatestApiEndpoints.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/maven/infrastructure/MavenLatestApiEndpoints.kt @@ -129,14 +129,21 @@ internal class MavenLatestApiEndpoints( ) private val findLatestFile = ReposiliteRoute("/api/maven/latest/file/{repository}/", GET) { accessed { - requireRepository { + requireRepository { repository -> response = resolveLatestArtifact( context = this@ReposiliteRoute, accessToken = this, - repository = it, + repository = repository, handler = { lookupRequest -> - mavenFacade.findFile(lookupRequest).map { (details, file) -> - ctx.resultAttachment(details.name, details.contentType, details.contentLength, compressionStrategy, file) + mavenFacade.findFile(lookupRequest).map { + ctx.resultAttachment( + name = it.document.name, + contentType = it.document.contentType, + contentLength = it.document.contentLength, + compressionStrategy = compressionStrategy, + cache = it.cachable, + data = it.content + ) } } ) diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/JavalinExtensions.kt b/reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/JavalinExtensions.kt index 06e2b0d39..2c8d7a416 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/JavalinExtensions.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/shared/extensions/JavalinExtensions.kt @@ -24,6 +24,7 @@ import io.javalin.http.ContentType import io.javalin.http.Context import io.javalin.http.HandlerType.HEAD import io.javalin.http.HandlerType.OPTIONS +import io.javalin.http.Header.CACHE_CONTROL import io.javalin.http.HttpStatus import org.eclipse.jetty.server.HttpOutput import panda.std.Result @@ -32,6 +33,7 @@ import java.io.InputStream import java.io.OutputStream import java.net.URLEncoder import java.nio.charset.Charset +import kotlin.time.Duration.Companion.hours internal class ContentTypeSerializer : StdSerializer { @@ -81,11 +83,14 @@ fun Context.response(result: Any): Context = } } +internal val maxAge = System.getProperty("reposilite.maven.maxAge", 1.hours.inWholeSeconds.toString()).toLong() + internal fun Context.resultAttachment( name: String, contentType: ContentType, contentLength: Long, compressionStrategy: String, + cache: Boolean, data: InputStream ) { if (!contentType.isHumanReadable) { @@ -96,6 +101,12 @@ internal fun Context.resultAttachment( contentLength(contentLength) // Using this with GZIP ends up with "Premature end of Content-Length delimited message body". } + if (cache) { + header(CACHE_CONTROL, "public, max-age=$maxAge") + } else { + header(CACHE_CONTROL, "no-cache, no-store, max-age=0") + } + when { acceptsBody() -> result(data) else -> data.silentClose() @@ -122,20 +133,6 @@ fun Context.encoding(encoding: String): Context = fun Context.contentDisposition(disposition: String): Context = header("Content-Disposition", disposition) -fun Context.resultAttachment(name: String, contentType: ContentType, contentLength: Long, data: InputStream): Context { - contentType(contentType) - - if (contentLength > 0) { - contentLength(contentLength) - } - - if (!contentType.isHumanReadable) { - contentDisposition(""""attachment; filename="$name" """) - } - - return response(data) -} - fun Context.uri(): String = path() diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/web/application/JavalinConfiguration.kt b/reposilite-backend/src/main/kotlin/com/reposilite/web/application/JavalinConfiguration.kt index 1dccf4408..9b95e31d8 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/web/application/JavalinConfiguration.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/web/application/JavalinConfiguration.kt @@ -34,7 +34,7 @@ import com.reposilite.token.AccessTokenFacade import com.reposilite.web.api.HttpServerConfigurationEvent import com.reposilite.web.api.HttpServerStartedEvent import com.reposilite.web.api.RoutingSetupEvent -import com.reposilite.web.infrastructure.CacheBypassHandler +import com.reposilite.web.infrastructure.ApiCacheBypassHandler import com.reposilite.web.infrastructure.EndpointAccessLoggingHandler import io.javalin.community.routing.dsl.DslExceptionHandler import io.javalin.community.routing.dsl.DslRoute @@ -79,7 +79,7 @@ internal object JavalinConfiguration { if (localConfiguration.bypassExternalCache.get()) { reposilite.extensions.registerEvent { event: RoutingSetupEvent -> event.registerRoutes(EndpointAccessLoggingHandler()) - event.registerRoutes(CacheBypassHandler()) + event.registerRoutes(ApiCacheBypassHandler()) reposilite.logger.debug("CacheBypassHandler has been registered") } } diff --git a/reposilite-backend/src/main/kotlin/com/reposilite/web/infrastructure/CacheBypassHandler.kt b/reposilite-backend/src/main/kotlin/com/reposilite/web/infrastructure/ApiCacheBypassHandler.kt similarity index 94% rename from reposilite-backend/src/main/kotlin/com/reposilite/web/infrastructure/CacheBypassHandler.kt rename to reposilite-backend/src/main/kotlin/com/reposilite/web/infrastructure/ApiCacheBypassHandler.kt index 9bcbae6ae..6e625dedd 100644 --- a/reposilite-backend/src/main/kotlin/com/reposilite/web/infrastructure/CacheBypassHandler.kt +++ b/reposilite-backend/src/main/kotlin/com/reposilite/web/infrastructure/ApiCacheBypassHandler.kt @@ -20,7 +20,7 @@ import com.reposilite.web.api.ReposiliteRoute import com.reposilite.web.api.ReposiliteRoutes import io.javalin.community.routing.Route.BEFORE -internal class CacheBypassHandler : ReposiliteRoutes() { +internal class ApiCacheBypassHandler : ReposiliteRoutes() { private val bypassCacheRoute = ReposiliteRoute("/api/*", BEFORE) { ctx.header("pragma", "no-cache") diff --git a/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt b/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt index 3e18c78bc..42a33abdc 100644 --- a/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt +++ b/reposilite-backend/src/test/kotlin/com/reposilite/maven/MavenFacadeTest.kt @@ -298,7 +298,7 @@ internal class MavenFacadeTest : MavenSpecification() { // then: valid pom xml has been generated and metadata file has been updated val pomBody = assertOk(mavenFacade.findFile(LookupRequest(token, repository.name, pom))) - .second + .content .use { it.readAllBytes().decodeToString() } assertThat(pomBody).isEqualTo( @@ -317,7 +317,7 @@ internal class MavenFacadeTest : MavenSpecification() { ) val metadataBody = assertOk(mavenFacade.findFile(LookupRequest(token, repository.name, gav.resolve(METADATA_FILE)))) - .second + .content .use { it.readAllBytes().decodeToString() } assertThat(metadataBody).contains("com.dzikoysk")