From: Lanius Trolling Date: Thu, 11 Apr 2024 11:00:49 +0000 (-0400) Subject: Implement copying files X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=83c6e6dcf3a6353e628289b2c9e8130d89325b20;p=factbooks Implement copying files --- diff --git a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt index 73ca5c5..ca30525 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt @@ -145,6 +145,9 @@ fun Application.factbooks() { exception { call, _ -> call.respondHtml(HttpStatusCode.NotFound, call.error404()) } + exception { call, _ -> + call.respondHtml(HttpStatusCode.NotFound, call.error404()) + } exception { call, _ -> call.respondHtml(HttpStatusCode.NotFound, call.error404()) } @@ -189,6 +192,8 @@ fun Application.factbooks() { get() get() get() + get() + post() postMultipart() postMultipart() get() diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/nations.kt b/src/jvmMain/kotlin/info/mechyrdia/data/nations.kt index 5f1e744..54d9fad 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/nations.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/nations.kt @@ -67,7 +67,7 @@ private val callCurrentNationAttribute = AttributeKey("CurrentNat fun ApplicationCall.ownerNationOnly() { if (sessions.get()?.nationId != OwnerNationId) - throw NullPointerException("Hidden page") + throw NoSuchElementException("Hidden page") } suspend fun ApplicationCall.currentNation(): NationData? { diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt b/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt index 86e88f7..e19bb69 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt @@ -95,7 +95,7 @@ suspend fun ApplicationCall.viewCommentRoute(commentId: Id): Nothing { val submitter = nationCache.getNation(comment.submittedBy) if (submitter.isBanned && currentNation?.id != comment.submittedBy && currentNation?.id != OwnerNationId) - throw NullPointerException("Shadowbanned comment") + throw NoSuchElementException("Shadowbanned comment") val pagePathParts = comment.submittedIn.split('/') val errorMessage = attributes.getOrNull(ErrorMessageAttributeKey) diff --git a/src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt b/src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt index f08579c..779ac89 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt @@ -146,7 +146,7 @@ suspend fun ApplicationCall.adminViewVfs(path: StoragePath): HTML.() -> Unit { td { ul { li { - a(href = href(Root.Admin.Vfs.Download(path.elements))) { + a(classes = "button", href = href(Root.Admin.Vfs.Download(path.elements))) { +"Download" } } @@ -161,7 +161,12 @@ suspend fun ApplicationCall.adminViewVfs(path: StoragePath): HTML.() -> Unit { } } li { - a(href = href(Root.Admin.Vfs.DeleteConfirmPage(path.elements))) { + a(classes = "button", href = href(Root.Admin.Vfs.CopyPage(path.elements))) { + +"Make Copy" + } + } + li { + a(classes = "button evil", href = href(Root.Admin.Vfs.DeleteConfirmPage(path.elements))) { +"Delete" } } @@ -225,6 +230,66 @@ suspend fun ApplicationCall.adminPreviewFile(path: StoragePath) { if (!result) respond(HttpStatusCode.NotFound) } +private suspend fun fileTreeForCopy(path: StoragePath): TreeNode.DirNode? { + return coroutineScope { + FileStorage.instance.listDir(path)?.map { entry -> + async { + fileTreeForCopy(path / entry.name)?.let { entry.name to it } + } + }?.awaitAll() + ?.filterNotNull() + ?.toMap() + ?.let { TreeNode.DirNode(it) } + } +} + +context(ApplicationCall) +private fun UL.renderForCopy(fromPath: StoragePath, intoPath: StoragePath, node: TreeNode.DirNode) { + li { + form(method = FormMethod.post, action = href(Root.Admin.Vfs.CopyPost(intoPath.elements))) { + installCsrfToken() + hiddenInput(name = "from") { value = fromPath.toString() } + submitInput { value = "Copy Into /$intoPath" } + } + ul { + for ((childName, childNode) in node.children) + if (childNode is TreeNode.DirNode) + renderForCopy(fromPath, intoPath / childName, childNode) + } + } +} + +suspend fun ApplicationCall.adminShowCopyFile(from: StoragePath): HTML.() -> Unit { + if (FileStorage.instance.statFile(from) == null) + throw NoSuchElementException("File does not exist") + + val tree = fileTreeForCopy(StoragePath.Root)!! + + return adminPage("Copy File /$from") { + main { + h1 { +"Choose Destination for /$from" } + ul { + li { + form(method = FormMethod.get, action = href(Root.Admin.Vfs.View(from.elements))) { + submitInput { value = "Cancel Copy" } + } + } + renderForCopy(from, StoragePath.Root, tree) + } + } + } +} + +suspend fun ApplicationCall.adminDoCopyFile(from: StoragePath, into: StoragePath) { + val name = from.elements.last() + val dest = into / name + + if (FileStorage.instance.copyFile(from, dest)) + redirectHref(Root.Admin.Vfs.View(dest.elements)) + else + respond(HttpStatusCode.Conflict) +} + suspend fun ApplicationCall.adminUploadFile(path: StoragePath, part: PartData.FileItem) { val name = part.originalFileName ?: throw MissingRequestParameterException("originalFileName") val filePath = path / name @@ -264,13 +329,18 @@ suspend fun ApplicationCall.adminConfirmDeleteFile(path: StoragePath) { } } - form(method = FormMethod.get, action = href(Root.Admin.Vfs.View(path.elements))) { - submitInput { value = "No, take me back" } - } - +Entities.nbsp - form(method = FormMethod.post, action = href(Root.Admin.Vfs.DeleteConfirmPost(path.elements))) { - installCsrfToken() - submitInput(classes = "evil") { value = "Yes, delete it" } + br + + div { + style = "text-align:center" + form(method = FormMethod.get, action = href(Root.Admin.Vfs.View(path.elements))) { + submitInput { value = "No, take me back" } + } + +Entities.nbsp + form(method = FormMethod.post, action = href(Root.Admin.Vfs.DeleteConfirmPost(path.elements))) { + installCsrfToken() + submitInput(classes = "evil") { value = "Yes, delete it" } + } } } }) @@ -312,13 +382,18 @@ suspend fun ApplicationCall.adminConfirmRemoveDirectory(path: StoragePath) { } } - form(method = FormMethod.get, action = href(Root.Admin.Vfs.View(path.elements))) { - submitInput { value = "No, take me back" } - } - +Entities.nbsp - form(method = FormMethod.post, action = href(Root.Admin.Vfs.RmDirConfirmPost(path.elements))) { - installCsrfToken() - submitInput(classes = "evil") { value = "Yes, delete it" } + br + + div { + style = "text-align:center" + form(method = FormMethod.get, action = href(Root.Admin.Vfs.View(path.elements))) { + submitInput { value = "No, take me back" } + } + +Entities.nbsp + form(method = FormMethod.post, action = href(Root.Admin.Vfs.RmDirConfirmPost(path.elements))) { + installCsrfToken() + submitInput(classes = "evil") { value = "Yes, delete it" } + } } } }) diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_bodies.kt b/src/jvmMain/kotlin/info/mechyrdia/route/resource_bodies.kt index 21508f8..30cc0ae 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/resource_bodies.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/resource_bodies.kt @@ -43,6 +43,9 @@ class AdminBanUserPayload(override val csrfToken: String? = null) : CsrfProtecte @Serializable class AdminUnbanUserPayload(override val csrfToken: String? = null) : CsrfProtectedResourcePayload +@Serializable +class AdminVfsCopyFilePayload(val from: String, override val csrfToken: String? = null) : CsrfProtectedResourcePayload + @Serializable class AdminVfsDeleteFilePayload(override val csrfToken: String? = null) : CsrfProtectedResourcePayload diff --git a/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt b/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt index e2f9549..f1a8d85 100644 --- a/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt +++ b/src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt @@ -304,6 +304,24 @@ class Root(val error: String? = null) : ResourceHandler, ResourceFilter { } } + @Resource("copy/{path...}") + class CopyPage(val path: List, val vfs: Vfs = Vfs()) : ResourceHandler { + override suspend fun PipelineContext.handleCall() { + with(vfs) { filterCall() } + + call.respondHtml(HttpStatusCode.OK, call.adminShowCopyFile(StoragePath(path))) + } + } + + @Resource("copy/{path...}") + class CopyPost(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { + override suspend fun PipelineContext.handleCall(payload: AdminVfsCopyFilePayload) { + with(vfs) { filterCall() } + + call.adminDoCopyFile(StoragePath(payload.from), StoragePath(path)) + } + } + @Resource("upload/{path...}") class Upload(val path: List, val vfs: Vfs = Vfs()) : ResourceReceiver { override suspend fun PipelineContext.handleCall(payload: CsrfProtectedMultiPartPayload) { diff --git a/src/jvmMain/resources/static/admin.css b/src/jvmMain/resources/static/admin.css index a8e1d29..1de150b 100644 --- a/src/jvmMain/resources/static/admin.css +++ b/src/jvmMain/resources/static/admin.css @@ -140,9 +140,13 @@ input[type=file] { display: none; } -label:has(> input[type=file]) { +a.button, label:has(> input[type=file]) { border: 1px solid #ec6; background-color: #541; + color: #fd7; + + text-shadow: 0 0 0.25em #fd7; + text-decoration: none; display: inline-block; vertical-align: middle; @@ -151,10 +155,23 @@ label:has(> input[type=file]) { cursor: pointer; } -label:has(> input[type=file]):hover { +a.button:hover, label:has(> input[type=file]):hover { background-color: #a82; } +a.button.evil { + border: 1px solid #d66; + background-color: #411; + color: #e77; + + text-shadow: 0 0 0.25em #b44; + text-decoration: none; +} + +a.button.evil:hover { + background-color: #922; +} + label:has(> input[type=file]) ~ input[type=submit] { display: none; } @@ -195,7 +212,7 @@ input[type=submit] { } input[type=submit].evil { - background-color: #922; + background-color: #811; } input[type=submit]:hover { @@ -209,7 +226,7 @@ input[type=submit]:hover { } input[type=submit].evil:hover { - background-color: #b44; + background-color: #a33; } input[type=submit]:active { @@ -223,5 +240,5 @@ input[type=submit]:active { } input[type=submit].evil:active { - background-color: #e77; + background-color: #d66; }