Skip to content

Commit

Permalink
GH-1662 Small refactor of frontend domain
Browse files Browse the repository at this point in the history
  • Loading branch information
dzikoysk committed Dec 29, 2022
1 parent 6786915 commit 0f403e2
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.reposilite.frontend

import panda.std.letIf

internal object BasePathFormatter {

private val pathRegex = Regex("^/|/$")

fun formatBasePath(originBasePath: String): String =
originBasePath
.letIf({ it.isNotEmpty() && !it.startsWith("/") }, { "/$it" })
.letIf({ it.isNotEmpty() && !it.endsWith("/")}, { "$it/" })

fun formatAsViteBasePath(path: String): String =
path
.takeIf { hasCustomBasePath(it) }
?.replace(pathRegex, "") // remove first & last slash
?: "." // no custom base path

private fun hasCustomBasePath(path: String): Boolean =
path != "" && path != "/"

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,50 +15,34 @@
*/
package com.reposilite.frontend

import com.reposilite.frontend.BasePathFormatter.formatAsViteBasePath
import com.reposilite.frontend.application.FrontendSettings
import com.reposilite.plugin.api.Facade
import panda.std.Result
import panda.std.letIf
import panda.std.reactive.Reference
import panda.std.reactive.computed
import java.io.IOException
import java.io.InputStream
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.StandardOpenOption

class FrontendFacade internal constructor(
basePath: Reference<String>,
private val frontendSettings: Reference<FrontendSettings>
) : Facade {

private val resources = HashMap<String, ResourceSupplier>(0)
private val formattedBasePath = basePath.computed { formatBasePath(it) }
private val formattedBasePath = basePath.computed { BasePathFormatter.formatBasePath(it) }

init {
computed(basePath, formattedBasePath, frontendSettings) {
resources.clear()
}
}

fun resolve(uri: String, source: () -> InputStream?): ResourceSupplier? =
resources[uri] ?: source()
?.let {
val temporaryResourcePath = Files.createTempFile("reposilite", "frontend-resource")
fun resolve(uri: String, source: Source): ResourceSupplier? =
resources[uri] ?: createProcessedResource(uri, source)

it.use { inputStream ->
Files.newOutputStream(temporaryResourcePath, StandardOpenOption.WRITE).use { outputStream ->
createLazyPlaceholderResolver().process(inputStream, outputStream)
}
}

ResourceSupplier {
Result.supplyThrowing(IOException::class.java) {
Files.newInputStream(temporaryResourcePath)
}
}
}
private fun createProcessedResource(uri: String, source: Source): ResourceSupplier? =
source.get()
?.let { createLazyPlaceholderResolver().createProcessedResource(it) }
?.also { resources[uri] = it }

private fun createLazyPlaceholderResolver(): LazyPlaceholderResolver =
Expand All @@ -67,8 +51,8 @@ class FrontendFacade internal constructor(
"{{REPOSILITE.BASE_PATH}}" to formattedBasePath.get(),
URLEncoder.encode("{{REPOSILITE.BASE_PATH}}", StandardCharsets.UTF_8) to formattedBasePath.get(),

"{{REPOSILITE.VITE_BASE_PATH}}" to getViteBasePath(),
URLEncoder.encode("{{REPOSILITE.VITE_BASE_PATH}}", StandardCharsets.UTF_8) to getViteBasePath(),
"{{REPOSILITE.VITE_BASE_PATH}}" to formatAsViteBasePath(formattedBasePath.get()),
URLEncoder.encode("{{REPOSILITE.VITE_BASE_PATH}}", StandardCharsets.UTF_8) to formatAsViteBasePath(formattedBasePath.get()),

"{{REPOSILITE.ID}}" to id,
"{{REPOSILITE.TITLE}}" to title,
Expand All @@ -79,27 +63,6 @@ class FrontendFacade internal constructor(
))
}

private fun formatBasePath(originBasePath: String): String =
originBasePath
.letIf({ it.isNotEmpty() && !it.startsWith("/") }, { "/$it" })
.letIf({ it.isNotEmpty() && !it.endsWith("/")}, { "$it/" })

private val pathRegex = Regex("^/|/$")

private fun getViteBasePath(): String =
formattedBasePath.get()
.takeIf { hasCustomBasePath() }
?.replace(pathRegex, "") // remove first & last slash
?: "." // no custom base path

private fun hasCustomBasePath(): Boolean =
formattedBasePath.map { it != "" && it != "/" }

private fun String.resolvePathPlaceholder(placeholder: String, value: String): String =
this
.replace(placeholder, value)
.replace(URLEncoder.encode(placeholder, StandardCharsets.UTF_8), URLEncoder.encode(value, StandardCharsets.UTF_8))

fun createNotFoundPage(originUri: String, details: String): String =
NotFoundTemplate.createNotFoundPage(formattedBasePath.get(), originUri, details)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
package com.reposilite.frontend

import panda.std.Result
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.StandardOpenOption

internal class LazyPlaceholderResolver(private val placeholders: Map<String, String>) {

init {
verifyPlaceholders()
}

private val theLongestPlaceholder = placeholders.keys
.maxOfOrNull { it }
?.length
?: 0

init {
verifyPlaceholders()
}
fun createProcessedResource(input: InputStream): ResourceSupplier {
val temporaryResourcePath = Files.createTempFile("reposilite", "frontend-resource")

private fun verifyPlaceholders() {
placeholders.keys.forEach { placeholder ->
placeholder.forEach {
if (it.code > Byte.MAX_VALUE) {
throw UnsupportedOperationException("LazyPlaceholderResolve supports only basic placeholders from 1-byte long symbols")
}
input.use { inputStream ->
Files.newOutputStream(temporaryResourcePath, StandardOpenOption.WRITE).use { outputStream ->
process(inputStream, outputStream)
}
}

return ResourceSupplier {
Result.supplyThrowing(IOException::class.java) {
Files.newInputStream(temporaryResourcePath)
}
}
}

fun process(input: InputStream, output: OutputStream) {
private fun process(input: InputStream, output: OutputStream) {
val buffer = ByteArray(1024)

while (true) {
Expand Down Expand Up @@ -76,4 +86,14 @@ internal class LazyPlaceholderResolver(private val placeholders: Map<String, Str
return content
}

private fun verifyPlaceholders() {
placeholders.keys.forEach { placeholder ->
placeholder.forEach {
if (it.code > Byte.MAX_VALUE) {
throw UnsupportedOperationException("LazyPlaceholderResolve supports only basic placeholders from 1-byte long symbols")
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import panda.std.Result
import java.io.IOException
import java.io.InputStream

fun interface Source {
fun get(): InputStream?
}

fun interface ResourceSupplier {
fun supply(): Result<InputStream, IOException>
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.reposilite.frontend.infrastructure

import com.reposilite.frontend.FrontendFacade
import com.reposilite.frontend.Source
import com.reposilite.shared.ErrorResponse
import com.reposilite.shared.extensions.encoding
import com.reposilite.shared.notFoundError
Expand All @@ -41,26 +42,26 @@ import kotlin.streams.asSequence

internal sealed class FrontendHandler(private val frontendFacade: FrontendFacade) : ReposiliteRoutes() {

protected fun respondWithFile(ctx: Context, uri: String, source: () -> InputStream?): Result<InputStream, ErrorResponse> {
protected fun respondWithResource(ctx: Context, uri: String, source: Source): Result<InputStream, ErrorResponse> {
val contentType = ContentType.getContentTypeByExtension(uri.getExtension())
ctx.contentType(contentType?.mimeType ?: ContentType.OCTET_STREAM)

return when (uri.contains(".html") || uri.contains(".js")) {
true -> respondWithProcessedFile(ctx, uri, source)
else -> respondWithRawFile(source)
true -> respondWithProcessedResource(ctx, uri, source)
else -> respondWithRawResource(source)
}
}

private fun respondWithProcessedFile(ctx: Context, uri: String, source: () -> InputStream?): Result<InputStream, ErrorResponse> =
frontendFacade.resolve(uri) { source() }
private fun respondWithProcessedResource(ctx: Context, uri: String, source: Source): Result<InputStream, ErrorResponse> =
frontendFacade.resolve(uri) { source.get() }
?.let {
ctx.encoding(UTF_8)
it.supply().mapErr { ErrorResponse(INTERNAL_SERVER_ERROR, "Cannot serve resource") }
}
?: notFoundError("Resource not found")

private fun respondWithRawFile(source: () -> InputStream?): Result<InputStream, ErrorResponse> =
source()
private fun respondWithRawResource(source: Source): Result<InputStream, ErrorResponse> =
source.get()
?.asSuccess()
?: notFoundError("Resource not found")

Expand All @@ -69,19 +70,19 @@ internal sealed class FrontendHandler(private val frontendFacade: FrontendFacade
internal class ResourcesFrontendHandler(frontendFacade: FrontendFacade, private val resourcesDirectory: String) : FrontendHandler(frontendFacade) {

private val defaultHandler = ReposiliteRoute<InputStream>("/", GET) {
response = respondWithResource(ctx, "index.html")
response = respondWithBundledResource(ctx, "index.html")
}

private val indexHandler = ReposiliteRoute<InputStream>("/index.html", GET) {
response = respondWithResource(ctx, "index.html")
response = respondWithBundledResource(ctx, "index.html")
}

private val assetsHandler = ReposiliteRoute<InputStream>("/assets/<path>", GET) {
response = respondWithResource(ctx, "assets/${ctx.pathParam("path")}")
response = respondWithBundledResource(ctx, "assets/${ctx.pathParam("path")}")
}

private fun respondWithResource(ctx: Context, uri: String): Result<InputStream, ErrorResponse> =
respondWithFile(ctx, uri) {
private fun respondWithBundledResource(ctx: Context, uri: String): Result<InputStream, ErrorResponse> =
respondWithResource(ctx, uri) {
FrontendFacade::class.java.getResourceAsStream("/$resourcesDirectory/$uri") ?: "".toByteArray().inputStream()
}

Expand All @@ -91,38 +92,42 @@ internal class ResourcesFrontendHandler(frontendFacade: FrontendFacade, private

internal class CustomFrontendHandler(frontendFacade: FrontendFacade, directory: Path) : FrontendHandler(frontendFacade) {

private fun rootFileHandler(file: Path) = ReposiliteRoute<InputStream>("/${file.getSimpleName()}", GET) {
response = respondWithResource(ctx, file.getSimpleName()) {
file.inputStream().orNull()
}
}

private fun directoryHandler(directory: Path) = ReposiliteRoute<InputStream>("/${directory.fileName}/<path>", GET) {
response = respondWithResource(ctx, directory.getSimpleName()) {
parameter("path")
.toLocation()
.toPath()
.map { path -> directory.resolve(path) }
.flatMap { path -> path.inputStream().mapErr { error -> error.message } }
.orNull()
}
}

private fun indexHandler(directory: Path) = ReposiliteRoute<InputStream>("/", GET) {
response = respondWithResource(ctx, "index.html") {
directory.resolve("index.html")
.inputStream()
.orNull()
}
}

override val routes =
Files.list(directory).use { staticDirectoryStream ->
staticDirectoryStream.asSequence()
.map {
if (it.isDirectory())
ReposiliteRoute<InputStream>("/${it.fileName}/<path>", GET) {
response = respondWithFile(ctx, it.getSimpleName()) {
parameter("path")
.toLocation()
.toPath()
.map { path -> it.resolve(path) }
.flatMap { path -> path.inputStream().mapErr { error -> error.message } }
.orNull()
}
}
else
ReposiliteRoute("/${it.getSimpleName()}", GET) {
response = respondWithFile(ctx, it.getSimpleName()) {
it.inputStream().orNull()
}
}
when {
it.isDirectory() -> directoryHandler(it)
else -> rootFileHandler(it)
}
}
.toMutableSet()
.also {
it.add(ReposiliteRoute("/", GET) {
response = respondWithFile(ctx, "index.html") {
directory.resolve("index.html")
.inputStream()
.orNull()
}
})
}
.also { it.add(indexHandler(directory)) }
.let { routes(*it.toTypedArray()) }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ internal class NotFoundHandler(private val frontendFacade: FrontendFacade) : Han
// It does not support async handlers
private val defaultNotFoundHandler: (Context) -> Unit = { ctx ->
if (ctx.resultInputStream() == null) {
ctx.status(NOT_FOUND).html(frontendFacade.createNotFoundPage(ctx.req().requestURI, ""))
ctx.html(frontendFacade.createNotFoundPage(ctx.req().requestURI, ""))
ctx.status(NOT_FOUND)
}
}

Expand Down
1 change: 1 addition & 0 deletions reposilite-backend/src/test/workspace/configuration.cdn
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ databaseThreadPool: 1
compressionStrategy: none
# Default idle timeout used by Jetty
idleTimeout: 30000

# Adds cache bypass headers to each request from /api/* scope served by this instance.
# Helps to avoid various random issues caused by proxy provides (e.g. Cloudflare) and browsers.
bypassExternalCache: true
Expand Down

0 comments on commit 0f403e2

Please sign in to comment.