val extension = path.elements.last().substringAfter('.', "")
val type = if (extension.isEmpty()) ContentType.Text.Plain else ContentType.defaultForFileExtension(extension)
- val result = fileStorage.readFile(path) { producer ->
- attributes.put(StoragePathAttributeKey, path)
- respondBytesWriter(contentType = type, contentLength = stat.size, producer = producer)
- }
+ val content = fileStorage.readFile(path) ?: return respond(HttpStatusCode.NotFound)
- if (!result) respond(HttpStatusCode.NotFound)
+ attributes.put(StoragePathAttributeKey, path)
+ respondBytes(content, type)
}
suspend fun ApplicationCall.respondStoredFile(path: StoragePath) {
suspend fun statFile(path: StoragePath): StoredFileStats?
- suspend fun writeFile(path: StoragePath, content: () -> ByteReadChannel): Boolean
-
suspend fun writeFile(path: StoragePath, content: ByteArray): Boolean
- suspend fun readFile(path: StoragePath, content: suspend (suspend ByteWriteChannel.() -> Unit) -> Unit): Boolean
-
suspend fun readFile(path: StoragePath): ByteArray?
suspend fun copyFile(source: StoragePath, target: StoragePath): Boolean
return StoredFileStats(Instant.ofEpochMilli(file.lastModified()), file.length())
}
- override suspend fun writeFile(path: StoragePath, content: () -> ByteReadChannel): Boolean {
- val file = resolveFile(path)
-
- return withContext(Dispatchers.IO) {
- if (createFile(file)) {
- file.writeChannel().use { content().copyTo(this) }
- true
- } else false
- }
- }
-
override suspend fun writeFile(path: StoragePath, content: ByteArray): Boolean {
val file = resolveFile(path)
}
}
- override suspend fun readFile(path: StoragePath, content: suspend (suspend ByteWriteChannel.() -> Unit) -> Unit): Boolean {
- val file = resolveFile(path)
- if (!file.isFile) return false
-
- content {
- file.readChannel().copyTo(this)
- }
-
- return true
- }
-
override suspend fun readFile(path: StoragePath): ByteArray? {
val file = resolveFile(path)
if (!file.isFile) return null
return StoredFileStats(file.updated, gridFsFile.length)
}
- override suspend fun writeFile(path: StoragePath, content: () -> ByteReadChannel): Boolean {
- if (path.isRoot) return false
- if (getSuffix(path) != null) return false
- if (countPrefix(path) > 0) return false
-
- val bytesPublisher = flow {
- content().consumeEachBufferRange { buffer, last ->
- emit(buffer.copy())
- !last
- }
- }.asPublisher(CoroutineName("grid-fs-writer") + Dispatchers.IO)
-
- val newId = bucket.uploadFromPublisher(path.elements.last(), bytesPublisher).awaitFirst()
- updateExact(path, newId)
- return true
- }
-
override suspend fun writeFile(path: StoragePath, content: ByteArray): Boolean {
if (path.isRoot) return false
if (getSuffix(path) != null) return false
return true
}
- override suspend fun readFile(path: StoragePath, content: suspend (suspend ByteWriteChannel.() -> Unit) -> Unit): Boolean {
- if (path.isRoot) return false
- val file = getExact(path) ?: return false
- val gridFsId = file.file
-
- content {
- bucket.downloadToPublisher(gridFsId).asFlow().collect { buffer ->
- writeFully(buffer)
- }
- }
-
- return true
- }
-
override suspend fun readFile(path: StoragePath): ByteArray? {
if (path.isRoot) return null
val file = getExact(path) ?: return null
val submittedIn: List<String>,
val submittedAt: Instant,
+ val submittedInTitle: String,
+
val numEdits: Int,
val lastEdit: Instant?,
return coroutineScope {
comments.map { comment ->
async {
- val nationData = nations.getNation(comment.submittedBy)
+ val nationDataAsync = async { nations.getNation(comment.submittedBy) }
+ val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn.split('/')).toFriendlyPathTitle() }
val htmlResult = comment.contents.parseAs(ParserTree::toCommentHtml)
CommentRenderData(
id = comment.id,
- submittedBy = nationData,
+ submittedBy = nationDataAsync.await(),
submittedIn = comment.submittedIn.split('/'),
submittedAt = comment.submittedAt,
+ submittedInTitle = pageTitleAsync.await(),
numEdits = comment.numEdits,
lastEdit = comment.lastEdit,
contentsRaw = comment.contents,
style = "font-size:1.5em;margin-top:2.5em"
+"On factbook "
a(href = href(Root.LorePage(comment.submittedIn))) {
- +comment.submittedIn.toFriendlyIndexTitle()
+ +comment.submittedInTitle
}
}
import io.ktor.server.plugins.*
import io.ktor.server.response.*
import io.ktor.utils.io.jvm.javaio.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.*
import kotlinx.html.*
private sealed class TreeNode {
suspend fun ApplicationCall.adminPreviewFile(path: StoragePath) {
attributes.put(PageDoNotCacheAttributeKey, true)
- val stat = FileStorage.instance.statFile(path) ?: return respond(HttpStatusCode.NotFound)
val extension = path.elements.last().substringAfterLast('.', "")
val type = if (extension in textExtensions) ContentType.Text.Plain else ContentType.defaultForFileExtension(extension)
- val result = FileStorage.instance.readFile(path) { producer ->
- respondBytesWriter(contentType = type, contentLength = stat.size, producer = producer)
- }
-
- if (!result) respond(HttpStatusCode.NotFound)
+ val result = FileStorage.instance.readFile(path) ?: return respond(HttpStatusCode.NotFound)
+ respondBytes(result, type)
}
private suspend fun fileTreeForCopy(path: StoragePath): TreeNode.DirNode? {
val name = part.originalFileName ?: throw MissingRequestParameterException("originalFileName")
val filePath = path / name
- if (FileStorage.instance.writeFile(filePath) { part.streamProvider().toByteReadChannel(Dispatchers.IO) })
+ val content = withContext(Dispatchers.IO) { part.streamProvider().readAllBytes() }
+ if (FileStorage.instance.writeFile(filePath, content))
redirectHref(Root.Admin.Vfs.View(filePath.elements))
else
respond(HttpStatusCode.Conflict)
}
suspend fun ApplicationCall.adminOverwriteFile(path: StoragePath, part: PartData.FileItem) {
- if (FileStorage.instance.writeFile(path) { part.streamProvider().toByteReadChannel(Dispatchers.IO) })
+ if (FileStorage.instance.writeFile(path, withContext(Dispatchers.IO) { part.streamProvider().readAllBytes() }))
redirectHref(Root.Admin.Vfs.View(path.elements))
else
respond(HttpStatusCode.Conflict)
import kotlinx.html.li
import kotlinx.html.ul
-data class ArticleNode(val name: String, val subNodes: List<ArticleNode>)
+data class ArticleNode(val name: String, val title: String, val subNodes: List<ArticleNode>)
suspend fun rootArticleNodeList(): List<ArticleNode> = StoragePath.articleDir.toArticleNode().subNodes
suspend fun StoragePath.toArticleNode(): ArticleNode = ArticleNode(
name,
+ toFriendlyPageTitle(),
coroutineScope {
val path = this@toArticleNode
FileStorage.instance.listDir(path)?.map {
if (node.isViewable)
list.li {
val nodePath = base + node.name
- a(href = href(Root.LorePage(nodePath, format))) { +node.name.toFriendlyPageTitle() }
+ a(href = href(Root.LorePage(nodePath, format))) { +node.title }
if (node.subNodes.isNotEmpty())
ul {
node.subNodes.renderInto(this, nodePath, format)
}
}
-fun List<String>.toFriendlyIndexTitle() = joinToString(separator = " - ") { part ->
- part.toFriendlyPageTitle()
+suspend fun StoragePath.toFriendlyPageTitle() = ArticleTitleCache.get(this)
+ ?: if (elements.size > 1)
+ elements.lastOrNull()?.split('-')?.joinToString(separator = " ") { word ->
+ word.lowercase().replaceFirstChar { it.titlecase() }
+ }.orEmpty()
+ else TOC_TITLE
+
+suspend fun StoragePath.toFriendlyPathTitle(): String {
+ val lorePath = elements.drop(1)
+ if (lorePath.isEmpty()) return TOC_TITLE
+
+ return lorePath.indices.drop(1).map { index ->
+ StoragePath(lorePath.take(index)).toFriendlyPageTitle()
+ }.joinToString(separator = " - ")
}
fun String.toFriendlyPageTitle() = split('-').joinToString(separator = " ") { word ->
--- /dev/null
+package info.mechyrdia.lore
+
+import info.mechyrdia.data.FileStorage
+import info.mechyrdia.data.StoragePath
+
+object ArticleTitleCache : FileDependentCache<String>() {
+ override suspend fun processFile(path: StoragePath): String? {
+ if (path.elements[0] != StoragePath.articleDir.elements[0])
+ return null
+
+ val bytes = FileStorage.instance.readFile(path) ?: return null
+ val text = String(bytes)
+
+ return ParserState
+ .parseText(text)
+ .filterIsInstance<ParserTreeNode.Tag>()
+ .first { it isTag "h1" }
+ .subNodes
+ .treeToText()
+ }
+}
NavLink(href(Root()), "Lore Intro"),
NavLink(href(Root.LorePage(emptyList())), TOC_TITLE),
) + path?.let { pathParts ->
- pathParts.dropLast(1).mapIndexed { i, part ->
- val subPath = pathParts.take(i + 1)
- NavLink(href(Root.LorePage(subPath)), part.toFriendlyPageTitle())
+ pathParts.drop(1).indices.map { i ->
+ val subPath = pathParts.take(i)
+ NavLink(href(Root.LorePage(subPath)), (StoragePath.articleDir / subPath).toFriendlyPageTitle())
}
}.orEmpty() + (currentNation()?.let { data ->
listOf(
object LoreArticleFormatSerializer : KeyedEnumSerializer<LoreArticleFormat>(LoreArticleFormat.entries, LoreArticleFormat::format)
suspend fun ApplicationCall.loreRawArticlePage(pagePath: List<String>): HTML.() -> Unit {
- val articleDir = StoragePath.articleDir
-
- val pageFile = articleDir / pagePath
+ val pageFile = StoragePath.articleDir / pagePath
val pageNode = pageFile.toArticleNode()
val parentPaths = if (pagePath.isEmpty())
emptyList()
else {
- val pathDirs = pagePath.dropLast(1)
- listOf(Root.LorePage(emptyList(), LoreArticleFormat.RAW_HTML) to TOC_TITLE) + pathDirs.mapIndexed { i, part ->
- Root.LorePage(pathDirs.take(i + 1), LoreArticleFormat.RAW_HTML) to part.toFriendlyPageTitle()
+ val pathDirs = pagePath.drop(1)
+ listOf(Root.LorePage(emptyList(), LoreArticleFormat.RAW_HTML) to TOC_TITLE) + pathDirs.indices.map { i ->
+ val prefixPath = pathDirs.take(i)
+ Root.LorePage(prefixPath, LoreArticleFormat.RAW_HTML) to (StoragePath.articleDir / prefixPath).toFriendlyPageTitle()
}
}
if (isValid) {
if (pageType == StoredFileType.DIRECTORY) {
- val title = pagePath.takeIf { it.isNotEmpty() }?.toFriendlyIndexTitle() ?: TOC_TITLE
-
- return rawPage(title) {
+ return rawPage(pageNode.title) {
breadCrumbs(parentPaths)
- h1 { +title }
+ h1 { +pageNode.title }
ul {
pageNode.subNodes.renderInto(this, pagePath, LoreArticleFormat.RAW_HTML)
}
}
}
- val title = pagePath.last().toFriendlyPageTitle()
-
- return rawPage(title) {
+ return rawPage(pageNode.title) {
breadCrumbs(parentPaths)
- h1 { +title }
+ h1 { +pageNode.title }
p {
+"This factbook does not exist. Would you like to "
a(href = href(Root.LorePage(emptyList(), LoreArticleFormat.RAW_HTML))) { +"return to the table of contents" }
if (pageType == StoredFileType.DIRECTORY) {
val navbar = standardNavBar(pagePath.takeIf { it.isNotEmpty() })
- val title = pagePath.takeIf { it.isNotEmpty() }?.toFriendlyIndexTitle() ?: TOC_TITLE
-
val sidebar = PageNavSidebar(
listOf(
- NavLink("#page-top", title, aClasses = "left"),
+ NavLink("#page-top", pageNode.title, aClasses = "left"),
NavLink("#comments", "Comments", aClasses = "left")
)
)
- return page(title, navbar, sidebar) {
+ return page(pageNode.title, navbar, sidebar) {
section {
a { id = "page-top" }
- h1 { +title }
+ h1 { +pageNode.title }
ul {
pageNode.subNodes.renderInto(this, pagePath, format = format)
}
}
}
- val title = pagePath.last().toFriendlyPageTitle()
+ val title = pageNode.title
val navbar = standardNavBar(pagePath)
val sidebar = PageNavSidebar(
listOf(
return page(title, navbar, sidebar) {
section {
a { id = "page-top" }
- h1 { +title }
+ h1 { +pageNode.title }
p {
+"This factbook does not exist. Would you like to "
a(href = href(Root())) { +"return to the index page" }