Implement copying files
authorLanius Trolling <lanius@laniustrolling.dev>
Thu, 11 Apr 2024 11:00:49 +0000 (07:00 -0400)
committerLanius Trolling <lanius@laniustrolling.dev>
Thu, 11 Apr 2024 11:00:49 +0000 (07:00 -0400)
src/jvmMain/kotlin/info/mechyrdia/Factbooks.kt
src/jvmMain/kotlin/info/mechyrdia/data/nations.kt
src/jvmMain/kotlin/info/mechyrdia/data/views_comment.kt
src/jvmMain/kotlin/info/mechyrdia/data/views_files.kt
src/jvmMain/kotlin/info/mechyrdia/route/resource_bodies.kt
src/jvmMain/kotlin/info/mechyrdia/route/resource_types.kt
src/jvmMain/resources/static/admin.css

index 73ca5c59b0fa0a224f5bc9bff940587e9635a223..ca30525e7399e69a85dea73991b356fc4a3ea669 100644 (file)
@@ -145,6 +145,9 @@ fun Application.factbooks() {
                exception<NullPointerException> { call, _ ->
                        call.respondHtml(HttpStatusCode.NotFound, call.error404())
                }
+               exception<NoSuchElementException> { call, _ ->
+                       call.respondHtml(HttpStatusCode.NotFound, call.error404())
+               }
                exception<IOException> { call, _ ->
                        call.respondHtml(HttpStatusCode.NotFound, call.error404())
                }
@@ -189,6 +192,8 @@ fun Application.factbooks() {
                get<Root.Admin.Vfs.Inline>()
                get<Root.Admin.Vfs.Download>()
                get<Root.Admin.Vfs.View>()
+               get<Root.Admin.Vfs.CopyPage>()
+               post<Root.Admin.Vfs.CopyPost, _>()
                postMultipart<Root.Admin.Vfs.Upload, _>()
                postMultipart<Root.Admin.Vfs.Overwrite, _>()
                get<Root.Admin.Vfs.DeleteConfirmPage>()
index 5f1e74487e4f0732d987bf734b07b366e3735fdc..54d9fadcb7e8e204c8f51861234ddb427cc988c6 100644 (file)
@@ -67,7 +67,7 @@ private val callCurrentNationAttribute = AttributeKey<NationSession>("CurrentNat
 
 fun ApplicationCall.ownerNationOnly() {
        if (sessions.get<UserSession>()?.nationId != OwnerNationId)
-               throw NullPointerException("Hidden page")
+               throw NoSuchElementException("Hidden page")
 }
 
 suspend fun ApplicationCall.currentNation(): NationData? {
index 86e88f71c5fe52296aceee94c8648683e41a8703..e19bb69308b2d8693b1cb9001b439d146c2913e9 100644 (file)
@@ -95,7 +95,7 @@ suspend fun ApplicationCall.viewCommentRoute(commentId: Id<Comment>): 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)
index f08579c7d810b1cc066573e4d9e4c31221a7b546..779ac89f200630cdf7ced32ebd9d4ba4780447c8 100644 (file)
@@ -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" }
+                                       }
                                }
                        }
                })
index 21508f8b75f9007880d6012878f3064c8ae313f4..30cc0ae3b1a5b947c94f83a4bc5c19c0997b318a 100644 (file)
@@ -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
 
index e2f9549c05dae5acb0f292b4e708d2d603b2043e..f1a8d852e1e8af3155594f65668b4ab8d1ecaafd 100644 (file)
@@ -304,6 +304,24 @@ class Root(val error: String? = null) : ResourceHandler, ResourceFilter {
                                }
                        }
                        
+                       @Resource("copy/{path...}")
+                       class CopyPage(val path: List<String>, val vfs: Vfs = Vfs()) : ResourceHandler {
+                               override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall() {
+                                       with(vfs) { filterCall() }
+                                       
+                                       call.respondHtml(HttpStatusCode.OK, call.adminShowCopyFile(StoragePath(path)))
+                               }
+                       }
+                       
+                       @Resource("copy/{path...}")
+                       class CopyPost(val path: List<String>, val vfs: Vfs = Vfs()) : ResourceReceiver<AdminVfsCopyFilePayload> {
+                               override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall(payload: AdminVfsCopyFilePayload) {
+                                       with(vfs) { filterCall() }
+                                       
+                                       call.adminDoCopyFile(StoragePath(payload.from), StoragePath(path))
+                               }
+                       }
+                       
                        @Resource("upload/{path...}")
                        class Upload(val path: List<String>, val vfs: Vfs = Vfs()) : ResourceReceiver<CsrfProtectedMultiPartPayload> {
                                override suspend fun PipelineContext<Unit, ApplicationCall>.handleCall(payload: CsrfProtectedMultiPartPayload) {
index a8e1d29af81fd85d6f287c3dfbd5bb4f4fadcecd..1de150ba9038487e8bd51915a5cc4254a1250262 100644 (file)
@@ -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;
 }