From: Lanius Trolling Date: Fri, 12 Apr 2024 00:32:33 +0000 (-0400) Subject: Fix VFS and page titling X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=5c86df61a0332f8cfcf323a01617377639127ff4;p=factbooks Fix VFS and page titling --- diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/data_files.kt b/src/jvmMain/kotlin/info/mechyrdia/data/data_files.kt index 48c35de..3891327 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/data_files.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/data_files.kt @@ -34,12 +34,10 @@ suspend fun ApplicationCall.respondStoredFile(fileStorage: FileStorage, path: St 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) { @@ -107,12 +105,8 @@ interface FileStorage { 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 @@ -225,17 +219,6 @@ private class FlatFileStorage(val root: File) : FileStorage { 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) @@ -247,17 +230,6 @@ private class FlatFileStorage(val root: File) : FileStorage { } } - 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 @@ -422,23 +394,6 @@ private class GridFsStorage(val table: DocumentTable, val bucket: G 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 @@ -453,20 +408,6 @@ private class GridFsStorage(val table: DocumentTable, val bucket: G 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 diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/view_comments.kt b/src/jvmMain/kotlin/info/mechyrdia/data/view_comments.kt index 3c86b79..09e1e03 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/view_comments.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/view_comments.kt @@ -19,6 +19,8 @@ data class CommentRenderData( val submittedIn: List, val submittedAt: Instant, + val submittedInTitle: String, + val numEdits: Int, val lastEdit: Instant?, @@ -32,14 +34,16 @@ data class CommentRenderData( 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, @@ -63,7 +67,7 @@ fun FlowContent.commentBox(comment: CommentRenderData, loggedInAs: Id - 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? { @@ -294,14 +287,15 @@ suspend fun ApplicationCall.adminUploadFile(path: StoragePath, part: PartData.Fi 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) diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt index 237b249..c036473 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/article_listing.kt @@ -14,12 +14,13 @@ import kotlinx.html.a import kotlinx.html.li import kotlinx.html.ul -data class ArticleNode(val name: String, val subNodes: List) +data class ArticleNode(val name: String, val title: String, val subNodes: List) suspend fun rootArticleNodeList(): List = StoragePath.articleDir.toArticleNode().subNodes suspend fun StoragePath.toArticleNode(): ArticleNode = ArticleNode( name, + toFriendlyPageTitle(), coroutineScope { val path = this@toArticleNode FileStorage.instance.listDir(path)?.map { @@ -44,7 +45,7 @@ fun List.renderInto(list: UL, base: List = emptyList(), for 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) @@ -53,8 +54,20 @@ fun List.renderInto(list: UL, base: List = emptyList(), for } } -fun List.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 -> diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/article_titles.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/article_titles.kt new file mode 100644 index 0000000..2e0ac9c --- /dev/null +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/article_titles.kt @@ -0,0 +1,21 @@ +package info.mechyrdia.lore + +import info.mechyrdia.data.FileStorage +import info.mechyrdia.data.StoragePath + +object ArticleTitleCache : FileDependentCache() { + 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() + .first { it isTag "h1" } + .subNodes + .treeToText() + } +} diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/view_nav.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/view_nav.kt index 61d7479..5262403 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/view_nav.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/view_nav.kt @@ -35,9 +35,9 @@ suspend fun ApplicationCall.standardNavBar(path: List? = null) = listOf( 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( diff --git a/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt b/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt index 9aa96aa..56098fe 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/lore/views_lore.kt @@ -62,17 +62,16 @@ enum class LoreArticleFormat(val format: String? = null) { object LoreArticleFormatSerializer : KeyedEnumSerializer(LoreArticleFormat.entries, LoreArticleFormat::format) suspend fun ApplicationCall.loreRawArticlePage(pagePath: List): 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() } } @@ -81,11 +80,9 @@ suspend fun ApplicationCall.loreRawArticlePage(pagePath: List): HTML.() 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) } @@ -106,11 +103,9 @@ suspend fun ApplicationCall.loreRawArticlePage(pagePath: List): 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" } @@ -144,19 +139,17 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List, format: Lore 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) } @@ -186,7 +179,7 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List, format: Lore } } - val title = pagePath.last().toFriendlyPageTitle() + val title = pageNode.title val navbar = standardNavBar(pagePath) val sidebar = PageNavSidebar( listOf( @@ -198,7 +191,7 @@ suspend fun ApplicationCall.loreArticlePage(pagePath: List, format: Lore 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" }