Skip to content

Commit

Permalink
GH-1662 Replace in-memory cache with fs for processed frontend files (R…
Browse files Browse the repository at this point in the history
…esolve #1662)
  • Loading branch information
dzikoysk committed Dec 29, 2022
1 parent 17fcc16 commit 6786915
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 112 deletions.
2 changes: 1 addition & 1 deletion .run/Run Reposilite - Test workspace.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<option name="MAIN_CLASS_NAME" value="com.reposilite.ReposiliteLauncherKt" />
<module name="reposilite-parent.reposilite-backend.main" />
<option name="PROGRAM_PARAMETERS" value="--token name:secret --level=DEBUG" />
<option name="VM_PARAMETERS" value="-Xmx32M" />
<option name="VM_PARAMETERS" value="-Xmx24M" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/reposilite-backend/src/test/workspace" />
<method v="2">
<option name="Make" enabled="true" />
Expand Down
1 change: 1 addition & 0 deletions reposilite-backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ dependencies {

val commonsCoded = "1.15"
api("commons-codec:commons-codec:$commonsCoded")
implementation("org.apache.commons:commons-text:1.10.0")

val jansi = "2.4.0"
implementation("org.fusesource.jansi:jansi:$jansi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,7 @@ class LocalConfiguration : Facade {

/* Cache */

@Description("", "# Keep processed frontend files in memory to improve response time")
val cacheContent = reference(true)

@Description("# Adds cache bypass headers to each request from /api/* scope served by this instance.")
@Description("", "# Adds cache bypass headers to each request from /api/* scope served by this instance.")
@Description("# Helps to avoid various random issues caused by proxy provides (e.g. Cloudflare) and browsers.")
val bypassExternalCache = reference(true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,75 @@ package com.reposilite.frontend

import com.reposilite.frontend.application.FrontendSettings
import com.reposilite.plugin.api.Facade
import org.intellij.lang.annotations.Language
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(
private val cacheContent: Reference<Boolean>,
basePath: Reference<String>,
private val frontendSettings: Reference<FrontendSettings>
) : Facade {

private val resources = HashMap<String, String>(0)
private val uriFormatter = Regex("/+") // exclude common typos from URI
private val regexAntiXss = Regex("[^A-Za-z0-9/.\\- ]") // exclude custom non-standard characters from template
private val pathRegex = Regex("^/|/$")

private val formattedBasePath = basePath.computed { // verify base path
var formattedBasePath = it

if (formattedBasePath.isNotEmpty()) {
if (!formattedBasePath.startsWith("/")) {
formattedBasePath = "/$formattedBasePath"
}
if (!formattedBasePath.endsWith("/")) {
formattedBasePath += "/"
}
}

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

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

fun resolve(uri: String, source: () -> String?): String? =
fun resolve(uri: String, source: () -> InputStream?): ResourceSupplier? =
resources[uri] ?: source()
?.let { frontendSettings.map { settings -> resolvePlaceholders(settings, it) } }
?.also { if (cacheContent.get()) resources[uri] = it }

private fun resolvePlaceholders(frontendSettings: FrontendSettings, source: String): String =
with(frontendSettings) {
source
.resolvePathPlaceholder("{{REPOSILITE.BASE_PATH}}", formattedBasePath.get())
.resolvePathPlaceholder("{{REPOSILITE.VITE_BASE_PATH}}", getViteBasePath())
.replace("{{REPOSILITE.ID}}", id)
.replace("{{REPOSILITE.TITLE}}", title)
.replace("{{REPOSILITE.DESCRIPTION}}", description)
.replace("{{REPOSILITE.ORGANIZATION_WEBSITE}}", organizationWebsite)
.replace("{{REPOSILITE.ORGANIZATION_LOGO}}", organizationLogo)
.replace("{{REPOSILITE.ICP_LICENSE}}", icpLicense)
?.let {
val temporaryResourcePath = Files.createTempFile("reposilite", "frontend-resource")

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

ResourceSupplier {
Result.supplyThrowing(IOException::class.java) {
Files.newInputStream(temporaryResourcePath)
}
}
}
?.also { resources[uri] = it }

private fun createLazyPlaceholderResolver(): LazyPlaceholderResolver =
with (frontendSettings.get()) {
LazyPlaceholderResolver(mapOf(
"{{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.ID}}" to id,
"{{REPOSILITE.TITLE}}" to title,
"{{REPOSILITE.DESCRIPTION}}" to description,
"{{REPOSILITE.ORGANIZATION_WEBSITE}}" to organizationWebsite,
"{{REPOSILITE.ORGANIZATION_LOGO}}" to organizationLogo,
"{{REPOSILITE.ICP_LICENSE}}" to icpLicense,
))
}

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() }
Expand All @@ -87,63 +100,7 @@ class FrontendFacade internal constructor(
.replace(placeholder, value)
.replace(URLEncoder.encode(placeholder, StandardCharsets.UTF_8), URLEncoder.encode(value, StandardCharsets.UTF_8))

fun createNotFoundPage(originUri: String, details: String): String {
val uri = originUri.replace(uriFormatter, "/")
val basePath = formattedBasePath.get()
val dashboardUrl = basePath + (if (basePath.endsWith("/")) "" else "/") + "#" + uri

@Language("html")
val response = """
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Reposilite - 404 Not Found</title>
</head>
<style>
body {
height: calc(100vh - 170px);
display: flex;
justify-content: center;
align-items: center;
font-family: Arial, Helvetica, sans-serif;
}
.error-view {
text-align: center;
width: 100vh;
height: 100px;
}
.spooky p {
margin-top: 0;
margin-bottom: 0;
font-size: 1.2rem;
font-weight: lighter;
}
a:link, a:visited {
color: rebeccapurple;
}
</style>
<body>
<div class='error-view'>
<h1 style="font-size: 1.5rem">
<span style="color: gray;">404︱</span>Resource not found
</h1>
${if (details.isEmpty()) "" else "<p><i>${regexAntiXss.replace(details, "")}</i></p>" }
<p>Looking for a dashboard?</p>
<div class="spooky">
<p>{\__/}</p>
<p>(●ᴗ●)</p>
<p>( >🥕</p>
</div>
<p>Visit <a href="$dashboardUrl" style="color: rebeccapurple; text-decoration: none;">$dashboardUrl</a></p>
</div>
</body>
</html>
"""

return response.trimIndent()
}
fun createNotFoundPage(originUri: String, details: String): String =
NotFoundTemplate.createNotFoundPage(formattedBasePath.get(), originUri, details)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.reposilite.frontend

import java.io.InputStream
import java.io.OutputStream

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

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

init {
verifyPlaceholders()
}

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")
}
}
}
}

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

while (true) {
val length = input.read(buffer)
.takeIf { it != -1 }
?: break

// convert data from buffer to text
var content = buffer.decodeToString(endIndex = length)

// check if current content may end with placeholder
if (content.length > theLongestPlaceholder) {
content = loadSlicedPlaceholders(input, content)
}

// resolve placeholders
for ((name, value) in placeholders) {
content = content.replace(name, value)
}

val resolvedContentAsBytes = content.toByteArray()
output.write(resolvedContentAsBytes, 0, resolvedContentAsBytes.size)
}
}

private fun loadSlicedPlaceholders(input: InputStream, content: String): String {
val borderlineStartIndex = content.length - theLongestPlaceholder + 1
val positionCount = content.length - borderlineStartIndex

for (index in 0 until positionCount) {
val placeholderStartIndex = borderlineStartIndex + index
val boundaryPlaceholder = content.substring(placeholderStartIndex, content.length)

for (placeholder in placeholders.keys) {
if (placeholder.startsWith(boundaryPlaceholder)) {
val missingBuffer = ByteArray(theLongestPlaceholder)
val missingLength = input.read(missingBuffer)

if (missingLength == -1) {
return content
}

val contentWithMissingPlaceholder = content + missingBuffer.decodeToString(endIndex = missingLength)
return loadSlicedPlaceholders(input, contentWithMissingPlaceholder)
}
}
}

return content
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.reposilite.frontend

import org.intellij.lang.annotations.Language

object NotFoundTemplate {

private val uriFormatter = Regex("/+") // exclude common typos from URI
private val regexAntiXss = Regex("[^A-Za-z0-9/.\\- ]") // exclude custom non-standard characters from template

fun createNotFoundPage(basePath: String, originUri: String, details: String): String {
val uri = originUri.replace(uriFormatter, "/")
val dashboardUrl = basePath + (if (basePath.endsWith("/")) "" else "/") + "#" + uri

@Language("html")
val response = """
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Reposilite - 404 Not Found</title>
</head>
<style>
body {
height: calc(100vh - 170px);
display: flex;
justify-content: center;
align-items: center;
font-family: Arial, Helvetica, sans-serif;
}
.error-view {
text-align: center;
width: 100vh;
height: 100px;
}
.spooky p {
margin-top: 0;
margin-bottom: 0;
font-size: 1.2rem;
font-weight: lighter;
}
a:link, a:visited {
color: rebeccapurple;
}
</style>
<body>
<div class='error-view'>
<h1 style="font-size: 1.5rem">
<span style="color: gray;">404︱</span>Resource not found
</h1>
${if (details.isEmpty()) "" else "<p><i>${regexAntiXss.replace(details, "")}</i></p>" }
<p>Looking for a dashboard?</p>
<div class="spooky">
<p>{\__/}</p>
<p>(●ᴗ●)</p>
<p>( >🥕</p>
</div>
<p>Visit <a href="$dashboardUrl" style="color: rebeccapurple; text-decoration: none;">$dashboardUrl</a></p>
</div>
</body>
</html>
"""

return response.trimIndent()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.reposilite.frontend

import panda.std.Result
import java.io.IOException
import java.io.InputStream

fun interface ResourceSupplier {
fun supply(): Result<InputStream, IOException>
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import com.reposilite.plugin.api.PluginComponents
import panda.std.reactive.Reference

class FrontendComponents(
private val cacheContent: Reference<Boolean>,
private val basePath: Reference<String>,
private val frontendSettings: Reference<FrontendSettings>
) : PluginComponents {

fun frontendFacade(): FrontendFacade =
FrontendFacade(
cacheContent = cacheContent,
basePath = basePath,
frontendSettings = frontendSettings
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ internal class FrontendPlugin : ReposilitePlugin() {
val frontendSettings = sharedConfigurationFacade.getDomainSettings<FrontendSettings>()

val frontendFacade = FrontendComponents(
cacheContent = localConfiguration.cacheContent,
basePath = localConfiguration.basePath,
frontendSettings = frontendSettings
).frontendFacade()
Expand Down

0 comments on commit 6786915

Please sign in to comment.