import info.mechyrdia.Configuration
import info.mechyrdia.FileStorageConfig
import info.mechyrdia.lore.StoragePathAttributeKey
+import info.mechyrdia.lore.forEachSuspend
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
private suspend fun getSuffix(fullPath: StoragePath, forDir: Boolean = false) = try {
val pathParts = fullPath.elements
- coroutineScope {
- val indices = (if (forDir) 0 else 1)..pathParts.lastIndex
-
- indices.map { index ->
- val path = StoragePath(pathParts.dropLast(index))
- launch {
- if (testExact(path)) throw FileAlreadyExistsException(path.toString())
- }
- }
+ val indices = (if (forDir) 0 else 1)..pathParts.lastIndex
+
+ indices.forEachSuspend { index ->
+ val path = StoragePath(pathParts.dropLast(index))
+ if (testExact(path)) throw FileAlreadyExistsException(path.toString())
}
null
)
).asFlow().map { it.objectId }.toSet()
- coroutineScope {
- unusedFiles.map { unusedFile ->
- launch {
- bucket.delete(unusedFile).awaitFirst()
- }
- }.joinAll()
+ unusedFiles.forEachSuspend { unusedFile ->
+ bucket.delete(unusedFile).awaitFirst()
}
}
import info.mechyrdia.Configuration
import info.mechyrdia.FileStorageConfig
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
+import info.mechyrdia.lore.mapSuspend
import kotlinx.coroutines.runBlocking
import kotlin.system.exitProcess
val inDir = from.listDir(path) ?: return listOf("[Source Error] Directory at /$path does not exist")
- return coroutineScope {
- inDir.map { (name, type) ->
- async {
- val subPath = path / name
- when (type) {
- StoredFileType.FILE -> migrateFile(subPath, from, into)
- StoredFileType.DIRECTORY -> migrateDir(subPath, from, into)
- }
- }
- }.awaitAll().flatten()
- }
+ return inDir.toList().mapSuspend { (name, type) ->
+ val subPath = path / name
+ when (type) {
+ StoredFileType.FILE -> migrateFile(subPath, from, into)
+ StoredFileType.DIRECTORY -> migrateDir(subPath, from, into)
+ }
+ }.flatten()
}
private suspend fun migrateRoot(from: FileStorage, into: FileStorage): List<String> {
- val inRoot = from.listDir(StoragePath.Root)
- ?: return listOf("[Source Error] Root directory does not exist")
+ val inRoot = from.listDir(StoragePath.Root) ?: return listOf("[Source Error] Root directory does not exist")
- return coroutineScope {
- inRoot.map { (name, type) ->
- async {
- val subPath = StoragePath.Root / name
- when (type) {
- StoredFileType.FILE -> migrateFile(subPath, from, into)
- StoredFileType.DIRECTORY -> migrateDir(subPath, from, into)
- }
- }
- }.awaitAll().flatten()
- }
+ return inRoot.toList().mapSuspend { (name, type) ->
+ val subPath = StoragePath.Root / name
+ when (type) {
+ StoredFileType.FILE -> migrateFile(subPath, from, into)
+ StoredFileType.DIRECTORY -> migrateDir(subPath, from, into)
+ }
+ }.flatten()
}
fun interface FileStorageMigrator {
import info.mechyrdia.route.installCsrfToken
import io.ktor.server.application.*
import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.html.*
import java.time.Instant
val replyLinks: List<Id<Comment>>,
) {
companion object {
+ private suspend fun render(comment: Comment, nations: MutableMap<Id<NationData>, NationData> = mutableMapOf()): CommentRenderData {
+ val (nationData, pageTitle, htmlResult) = coroutineScope {
+ val nationDataAsync = async { nations.getNation(comment.submittedBy) }
+ val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn).toFriendlyPathTitle() }
+ val htmlResultAsync = async { comment.contents.parseAs(ParserTree::toCommentHtml) }
+
+ Triple(nationDataAsync.await(), pageTitleAsync.await(), htmlResultAsync.await())
+ }
+
+ return CommentRenderData(
+ id = comment.id,
+ submittedBy = nationData,
+ submittedIn = comment.submittedIn.split('/'),
+ submittedAt = comment.submittedAt,
+ submittedInTitle = pageTitle,
+ numEdits = comment.numEdits,
+ lastEdit = comment.lastEdit,
+ contentsRaw = comment.contents,
+ contentsHtml = htmlResult,
+ replyLinks = CommentReplyLink.getReplies(comment.id),
+ )
+ }
+
suspend operator fun invoke(comments: List<Comment>, nations: MutableMap<Id<NationData>, NationData> = mutableMapOf()): List<CommentRenderData> {
- return coroutineScope {
- comments.map { comment ->
- async {
- val nationDataAsync = async { nations.getNation(comment.submittedBy) }
- val pageTitleAsync = async { (StoragePath.articleDir / comment.submittedIn).toFriendlyPathTitle() }
- val htmlResult = comment.contents.parseAs(ParserTree::toCommentHtml)
-
- CommentRenderData(
- id = comment.id,
- submittedBy = nationDataAsync.await(),
- submittedIn = comment.submittedIn.split('/'),
- submittedAt = comment.submittedAt,
- submittedInTitle = pageTitleAsync.await(),
- numEdits = comment.numEdits,
- lastEdit = comment.lastEdit,
- contentsRaw = comment.contents,
- contentsHtml = htmlResult,
- replyLinks = CommentReplyLink.getReplies(comment.id),
- )
- }
- }.awaitAll()
+ return comments.mapSuspend { comment ->
+ render(comment, nations)
}
}
}
import info.mechyrdia.auth.PageDoNotCacheAttributeKey
import info.mechyrdia.lore.adminPage
import info.mechyrdia.lore.dateTime
+import info.mechyrdia.lore.mapSuspend
import info.mechyrdia.lore.redirectHref
import info.mechyrdia.route.Root
import info.mechyrdia.route.href
import io.ktor.server.html.*
import io.ktor.server.plugins.*
import io.ktor.server.response.*
-import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
import kotlinx.html.*
fun Map<String, StoredFileType>.sortedAsFiles() = toList()
private suspend fun fileTree(path: StoragePath): TreeNode? {
return FileStorage.instance.statFile(path)?.let {
TreeNode.FileNode(it)
- } ?: coroutineScope {
- FileStorage.instance.listDir(path)?.map { (name, _) ->
- async {
- fileTree(path / name)?.let { name to it }
- }
- }?.awaitAll()
- ?.filterNotNull()
- ?.toMap()
- ?.let { TreeNode.DirNode(it) }
- }
+ } ?: FileStorage.instance.listDir(path)?.keys?.mapSuspend { name ->
+ fileTree(path / name)?.let { name to it }
+ }?.filterNotNull()?.toMap()?.let { TreeNode.DirNode(it) }
}
context(ApplicationCall)
}
private suspend fun fileTreeForCopy(path: StoragePath): TreeNode.DirNode? {
- return coroutineScope {
- FileStorage.instance.listDir(path)?.map { (name, _) ->
- async {
- fileTreeForCopy(path / name)?.let { name to it }
- }
- }?.awaitAll()
- ?.filterNotNull()
- ?.toMap()
- ?.let { TreeNode.DirNode(it) }
- }
+ return FileStorage.instance.listDir(path)?.keys?.mapSuspend { name ->
+ fileTreeForCopy(path / name)?.let { name to it }
+ }?.filterNotNull()?.toMap()?.let { TreeNode.DirNode(it) }
}
context(ApplicationCall)
import info.mechyrdia.route.Root
import info.mechyrdia.route.href
import io.ktor.server.application.*
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
import kotlinx.html.UL
import kotlinx.html.a
import kotlinx.html.li
suspend fun StoragePath.toArticleNode(): ArticleNode = ArticleNode(
name,
toFriendlyPageTitle(),
- coroutineScope {
- val path = this@toArticleNode
- FileStorage.instance.listDir(path)?.map { (name, _) ->
- val subPath = path / name
- async { subPath.toArticleNode() }
- }?.awaitAll()
+ FileStorage.instance.listDir(this)?.keys?.mapSuspend { name ->
+ (this / name).toArticleNode()
}?.sortedAsArticles()
)
import info.mechyrdia.route.KeyedEnumSerializer
import info.mechyrdia.yieldThread
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import org.slf4j.Logger
suspend fun renderTextToSvg(text: String, bold: Boolean, italic: Boolean, align: TextAlignment): SvgDoc {
val (file, font) = getFont(bold, italic)
- val shape = layoutText(text, file, font, align)
- return createSvgDocument(shape, 80.0 / file.unitsPerEm, 12.0)
+
+ return runInterruptible(Dispatchers.Default) {
+ val shape = layoutText(text, file, font, align)
+ createSvgDocument(shape, 80.0 / file.unitsPerEm, 12.0)
+ }
}
private val fontsRoot = StoragePath("fonts")
package info.mechyrdia.lore
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
-
data class AsyncLexerTagEnvironment<TContext, TSubject>(
val context: TContext,
private val processTags: AsyncLexerTags<TContext, TSubject>,
fun interface AsyncLexerCombiner<TContext, TSubject> {
suspend fun processAndCombine(env: AsyncLexerTagEnvironment<TContext, TSubject>, nodes: ParserTree): TSubject {
- return combine(env, coroutineScope {
- nodes.map {
- async { env.processNode(it) }
- }.awaitAll()
+ return combine(env, nodes.mapSuspend {
+ env.processNode(it)
})
}
import info.mechyrdia.data.StoragePath
import io.ktor.server.application.*
import io.ktor.server.request.*
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.*
import java.time.Instant
import kotlin.math.roundToInt
it.param to it.subNodes
}.toMap()
+suspend fun <T> Iterable<T>.forEachSuspend(processor: suspend (T) -> Unit) = coroutineScope {
+ map {
+ launch {
+ processor(it)
+ }
+ }.joinAll()
+}
+
suspend fun <T, R> Iterable<T>.mapSuspend(processor: suspend (T) -> R) = coroutineScope {
map {
async {
import info.mechyrdia.data.*
import io.ktor.http.*
import io.ktor.server.application.*
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
val stat = statAll(listOf(path, dataPath))
return if (stat != null)
listOf(StoragePathWithStat(path, stat))
- else if (subNodes != null) coroutineScope {
- subNodes.map { subNode ->
- async {
- subNode.getPages(path)
- }
- }.awaitAll().flatten()
- } else emptyList()
+ else subNodes?.mapSuspend { subNode ->
+ subNode.getPages(path)
+ }?.flatten().orEmpty()
}
suspend fun allPages(): List<StoragePathWithStat> {
- return coroutineScope {
- rootArticleNodeList().map { subNode ->
- async {
- subNode.getPages(StoragePath.articleDir)
- }
- }.awaitAll().flatten()
- }
+ return rootArticleNodeList().mapSuspend { subNode ->
+ subNode.getPages(StoragePath.articleDir)
+ }.flatten()
}
suspend fun generateRecentPageEdits(): RssChannel {
categories = listOf(
RssCategory(domain = "https://nationstates.net", category = "Mechyrdia")
),
- items = coroutineScope {
- pages.map { page ->
- async {
- val pageLink = page.path.elements.drop(1)
- val pageMarkup = FactbookLoader.loadFactbook(pageLink) ?: return@async null
-
- val pageToC = TableOfContentsBuilder()
- pageMarkup.buildToC(pageToC)
- val pageOg = pageToC.toOpenGraph()
-
- val imageEnclosure = pageOg?.image?.let { url ->
- val assetPath = url.removePrefix("$MainDomainName/assets/")
- val file = StoragePath.assetDir / assetPath
- RssItemEnclosure(
- url = url,
- length = FileStorage.instance.statFile(file)?.size ?: 0L,
- type = ContentType.defaultForFileExtension(assetPath.substringAfterLast('.')).toString()
- )
- }
-
- RssItem(
- title = pageToC.toPageTitle(),
- description = pageOg?.description,
- link = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}",
- author = null,
- comments = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}#comments",
- enclosure = imageEnclosure,
- pubDate = page.stat.updated
- )
- }
- }.awaitAll().filterNotNull()
- }
+ items = pages.mapSuspend { page ->
+ val pageLink = page.path.elements.drop(1)
+ val pageMarkup = FactbookLoader.loadFactbook(pageLink) ?: return@mapSuspend null
+
+ val pageToC = TableOfContentsBuilder()
+ pageMarkup.buildToC(pageToC)
+ val pageOg = pageToC.toOpenGraph()
+
+ val imageEnclosure = pageOg?.image?.let { url ->
+ val assetPath = url.removePrefix("$MainDomainName/assets/")
+ val file = StoragePath.assetDir / assetPath
+ RssItemEnclosure(
+ url = url,
+ length = FileStorage.instance.statFile(file)?.size ?: 0L,
+ type = ContentType.defaultForFileExtension(assetPath.substringAfterLast('.')).toString()
+ )
+ }
+
+ RssItem(
+ title = pageToC.toPageTitle(),
+ description = pageOg?.description,
+ link = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}",
+ author = null,
+ comments = "$MainDomainName/lore${pageLink.joinToString(separator = "") { "/$it" }}#comments",
+ enclosure = imageEnclosure,
+ pubDate = page.stat.updated
+ )
+ }.filterNotNull()
)
}
import info.mechyrdia.auth.WebDavToken
import info.mechyrdia.auth.toNationId
import info.mechyrdia.data.*
+import info.mechyrdia.lore.mapSuspend
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.*
-import io.ktor.utils.io.core.*
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
import kotlinx.html.*
import java.net.URI
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.*
-import kotlin.text.String
const val WebDavDomainName = "https://dav.mechyrdia.info"
)
} ?: FileStorage.instance.listDir(path)?.let { subEntries ->
val subPaths = subEntries.keys.map { path / it }
- val subProps = coroutineScope {
- subPaths.map { subPath ->
- async {
- getWebDavPropertiesWithIncludeTags(subPath, webRoot, depth - 1)
- }
- }.awaitAll().filterNotNull().flatten()
- }
+ val subProps = subPaths.mapSuspend { subPath ->
+ getWebDavPropertiesWithIncludeTags(subPath, webRoot, depth - 1)
+ }.filterNotNull().flatten()
val pathWithSuffix = path.elements.joinToString(separator = "") { "$it/" }
listOf(
return when (getType(source)) {
StoredFileType.DIRECTORY -> createDir(target) && (listDir(source)?.let { subPaths ->
val copyActions = subPaths.keys.map { (source / it) to (target / it) }
- coroutineScope {
- copyActions.map { (subSource, subTarget) ->
- async { copyWebDav(subSource, subTarget) }
- }.awaitAll().all { it }
- }
+ copyActions.mapSuspend { (subSource, subTarget) ->
+ copyWebDav(subSource, subTarget)
+ }.all { it }
} == true)
StoredFileType.FILE -> copyFile(source, target)