Skip to content

Commit

Permalink
Use file hash instead of base64 for media identifier
Browse files Browse the repository at this point in the history
  • Loading branch information
iamgio committed Aug 28, 2024
1 parent adb836a commit 15889c0
Show file tree
Hide file tree
Showing 5 changed files with 27 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,25 @@ package eu.iamgio.quarkdown.media.storage.name

import eu.iamgio.quarkdown.media.LocalMedia
import eu.iamgio.quarkdown.media.RemoteMedia
import eu.iamgio.quarkdown.util.sanitize
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import eu.iamgio.quarkdown.util.sanitizeFileName

/**
* A media name generator that sanitizes the file name.
* For example, "my file.jpg" is mapped to "<parent>-my-file.jpg"
* A media name generator that sanitizes the file name and includes a unique identifier in it.
* For example, "path/to/my file.jpg" is mapped to "my-file@HASH.jpg"
*/
@OptIn(ExperimentalEncodingApi::class)
class SanitizedMediaNameProvider : MediaNameProviderStrategy {
private fun String.sanitize() = this.sanitize(replacement = "-")
private fun String.sanitize() = this.sanitizeFileName(replacement = "-")

// Local media are given a unique identifier based on their file name and hash code.
override fun visit(media: LocalMedia) =
buildString {
append(media.file.nameWithoutExtension)
append("-")
append(Base64.UrlSafe.encode(media.file.canonicalPath.toByteArray()))
append("@")
append(media.file.hashCode())
append(".")
append(media.file.extension)
}.sanitize()

// URLs are already unique, and they don't need an additional identifier.
override fun visit(media: RemoteMedia) = media.url.toExternalForm().sanitize()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package eu.iamgio.quarkdown.pipeline.output

import eu.iamgio.quarkdown.util.sanitize
import eu.iamgio.quarkdown.util.sanitizeFileName
import java.io.File

/**
Expand All @@ -15,25 +15,25 @@ class FileResourceExporter(private val location: File) : OutputResourceVisitor<F
private val OutputResource.fileName: String
get() =
name.replace("\\s+".toRegex(), "-")
.sanitize(replacement = "")
.sanitizeFileName(replacement = "")

/**
* File extension relative to the [ArtifactType] of this resource.
*/
private val TypedOutputResource.fileExtension: String
get() =
when (type) {
ArtifactType.HTML -> "html"
ArtifactType.CSS -> "css"
ArtifactType.JAVASCRIPT -> "js"
ArtifactType.HTML -> ".html"
ArtifactType.CSS -> ".css"
ArtifactType.JAVASCRIPT -> ".js"
ArtifactType.AUTO -> "" // Assumes the file name already contains an extension.
}

/**
* Full name of the file, including the extension, relative to the [ArtifactType] of this resource.
* Full name of the file, including the extension relative to the [ArtifactType] of this resource.
*/
private val TypedOutputResource.fullFileName: String
get() = "$fileName.$fileExtension"
get() = fileName + fileExtension

/**
* Saves an [OutputArtifact] to a file with text content.
Expand All @@ -45,7 +45,7 @@ class FileResourceExporter(private val location: File) : OutputResourceVisitor<F
}

override fun visit(artifact: BinaryOutputArtifact) =
File(location, artifact.fullFileName).also {
File(location, artifact.fileName).also {
it.writeBytes(artifact.content)
}

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/eu/iamgio/quarkdown/util/StringUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ fun StringBuilder.replace(

/**
* @return [this] string with all non-alphanumeric characters,
* except for `-`, `_`, and `.`, replaced with [replacement]
* except for `-`, `_`, `@`, and `.`, replaced with [replacement]
* @param replacement character to replace invalid characters with
*/
fun String.sanitize(replacement: String) = this.replace("[^a-zA-Z0-9\\-_.]+".toRegex(), replacement)
fun String.sanitizeFileName(replacement: String) = this.replace("[^a-zA-Z0-9\\-_.@]+".toRegex(), replacement)

/**
* @return [this] string with line separators replaced with `\n`,
Expand Down
14 changes: 7 additions & 7 deletions core/src/test/kotlin/eu/iamgio/quarkdown/MediaTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class MediaTest {
assertEquals(1, localOnlyStorage.all.size)

val stored = localOnlyStorage.all.first()
assertTrue(stored.name.startsWith("icon-"))
assertTrue(stored.name.startsWith("icon@"))
assertTrue(stored.name.endsWith(".png"))

localOnlyStorage.resolve("media/icon.png")?.let { resolved ->
Expand All @@ -82,7 +82,7 @@ class MediaTest {
}

localOnlyStorage.resolve("media/banner.png")?.let { resolved ->
assertTrue(resolved.name.startsWith("banner-"))
assertTrue(resolved.name.startsWith("banner@"))
assertTrue(resolved.name.endsWith(".png"))
}

Expand Down Expand Up @@ -113,7 +113,7 @@ class MediaTest {
}

localAndRemoteStorage.resolve("media/banner.png")?.let { resolved ->
assertTrue(resolved.name.startsWith("banner-"))
assertTrue(resolved.name.startsWith("banner@"))
assertTrue(resolved.name.endsWith(".png"))
}
}
Expand Down Expand Up @@ -175,7 +175,7 @@ class MediaTest {
assertEquals(2, storage.all.size)

storage.resolve("media/icon.png")?.let { resolved ->
assertTrue(resolved.name.startsWith("icon-"))
assertTrue(resolved.name.startsWith("icon@"))
assertTrue(resolved.name.endsWith(".png"))
}

Expand Down Expand Up @@ -222,7 +222,7 @@ class MediaTest {
height = null,
)

assertTrue(localImage.accept(renderer).startsWith("<img src=\"media/icon-"))
assertTrue(localImage.accept(renderer).startsWith("<img src=\"media/icon@"))

MutableContext(QuarkdownFlavor).let { localOnlyContext ->
localOnlyContext.options.enableLocalMediaStorage = true
Expand All @@ -246,7 +246,7 @@ class MediaTest {

localOnlyContext.mediaStorage.register("media/icon.png", workingDirectory = File("src/test/resources"))

assertTrue(localImage.accept(localOnlyRenderer).startsWith("<img src=\"media/icon-"))
assertTrue(localImage.accept(localOnlyRenderer).startsWith("<img src=\"media/icon@"))
}
}

Expand All @@ -270,7 +270,7 @@ class MediaTest {
assertIs<OutputResourceGroup>(resource)
assertEquals(3, resource.resources.size)

resource.resources.first { it.name.startsWith("icon-") }.let { icon ->
resource.resources.first { it.name.startsWith("icon@") }.let { icon ->
assertIs<BinaryOutputArtifact>(icon)
assertEquals(storage.resolve("media/icon.png")?.name, icon.name)
assertTrue(File("src/test/resources/media/icon.png").readBytes().contentEquals(icon.content))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ class FullPipelineTest {
""".trimIndent(),
enableMediaStorage = true,
) {
assertEquals("<p>This is the Quarkdown logo: <img src=\"media/icon", it.toString().substringBefore("-"))
assertEquals("<p>This is the Quarkdown logo: <img src=\"media/icon", it.toString().substringBefore("@"))
// The file name is "media/icon-[encoded].png"
assertEquals("\" alt=\"Quarkdown\" />.</p>", it.toString().substringAfter(".png"))
}
Expand Down Expand Up @@ -1062,7 +1062,7 @@ class FullPipelineTest {
"<p>" +
"<img src=\"https://raw.githubusercontent.com/iamgio/quarkdown/project-files/images/tbanner-light.svg\" " +
"alt=\"Banner\" /><br /><img src=\"media/",
it.toString().substringBefore("icon-"),
it.toString().substringBefore("icon@"),
)

assertEquals(1, mediaStorage.all.size)
Expand Down

0 comments on commit 15889c0

Please sign in to comment.