Skip to content

Commit

Permalink
GH-1977 Enable cache headers on immutable repositories (Resolve #1977)
Browse files Browse the repository at this point in the history
  • Loading branch information
dzikoysk committed Nov 11, 2023
1 parent 299ffb3 commit 090591c
Show file tree
Hide file tree
Showing 13 changed files with 70 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -48,14 +50,15 @@ 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()

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -52,7 +51,7 @@ class MavenFacade internal constructor(
fun findDetails(lookupRequest: LookupRequest): Result<out FileDetails, ErrorResponse> =
repositoryService.findDetails(lookupRequest)

fun findFile(lookupRequest: LookupRequest): Result<Pair<DocumentInfo, InputStream>, ErrorResponse> =
fun findFile(lookupRequest: LookupRequest): Result<ResolvedDocument, ErrorResponse> =
repositoryService.findFile(lookupRequest)

fun deployFile(deployRequest: DeployRequest): Result<Unit, ErrorResponse> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit, ErrorResponse> =
writeFileChecksums(location, bytes.inputStream())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -110,8 +110,17 @@ internal class RepositoryService(
fun findDetails(lookupRequest: LookupRequest): Result<FileDetails, ErrorResponse> =
resolve(lookupRequest) { repository, gav -> findDetails(lookupRequest.accessToken, repository, gav) }

fun findFile(lookupRequest: LookupRequest): Result<Pair<DocumentInfo, InputStream>, ErrorResponse> =
resolve(lookupRequest) { repository, gav -> findFile(lookupRequest.accessToken, repository, gav) }
fun findFile(lookupRequest: LookupRequest): Result<ResolvedDocument, ErrorResponse> =
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 <T> resolve(lookupRequest: LookupRequest, block: (Repository, Location) -> Result<T, ErrorResponse>): Result<T, ErrorResponse> {
val (accessToken, repositoryName, gav) = lookupRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,9 @@ data class ResolvedFileEvent(
val gav: Location,
var result: Result<Pair< DocumentInfo, InputStream>, ErrorResponse>
) : Event

data class ResolvedDocument(
val document: DocumentInfo,
val content: InputStream,
val cachable: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,21 @@ internal class MavenLatestApiEndpoints(
)
private val findLatestFile = ReposiliteRoute<Unit>("/api/maven/latest/file/{repository}/<gav>", 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
)
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<ContentType> {

Expand Down Expand Up @@ -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) {
Expand All @@ -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()
Expand All @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit>("/api/*", BEFORE) {
ctx.header("pragma", "no-cache")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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("<groupId>com.dzikoysk</groupId>")
Expand Down

0 comments on commit 090591c

Please sign in to comment.