Skip to content

Commit

Permalink
Added file renaming
Browse files Browse the repository at this point in the history
  • Loading branch information
carrotmargh committed Jan 4, 2023
1 parent da75b27 commit 9462351
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 21 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ You can of course also use the docker image with Kubernetes or Docker Compose.

- IntelliJ IDE or Gradle
- MySQL or MariaDB as a database
- JDK 17+

This project uses [Jooq](https://www.jooq.org/) in combination
with [Flyway](https://flywaydb.org/) to migrate an existing
Expand Down
8 changes: 5 additions & 3 deletions client/src/main/kotlin/ApiClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import com.perpheads.files.wrappers.axios
import com.perpheads.files.wrappers.axiosDelete
import com.perpheads.files.wrappers.axiosGet
import com.perpheads.files.wrappers.axiosPost
import com.perpheads.files.data.ShareFileResponse
import kotlinx.browser.window
import kotlinx.coroutines.MainScope
import kotlinx.css.em
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.w3c.files.File
Expand Down Expand Up @@ -57,6 +54,11 @@ object ApiClient {
return axiosDelete("/${link}")
}

suspend fun renameFile(link: String, newName: String) {
val body = RenameFileRequest(newName)
return axiosPost("/${link}", body)
}

suspend fun getAccountInfo(): AccountInfoV2 {
return axiosGet("/account-info")
}
Expand Down
15 changes: 15 additions & 0 deletions client/src/main/kotlin/components/AccountPageComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ val AccountPageComponent = fc<AccountPageProps>("AccountPageComponent") {
}
}

suspend fun doRename(file: FileResponse, newName: String) {
logoutIfUnauthorized(navigate) {
ApiClient.renameFile(file.link, newName)
}
}

useEffect(location) {
ApiClient.mainScope.launch {
loadFiles()
Expand Down Expand Up @@ -134,6 +140,15 @@ val AccountPageComponent = fc<AccountPageProps>("AccountPageComponent") {
files = newFiles
}
}
renameFile = { file, newName ->
ApiClient.mainScope.launch {
doRename(file, newName)
val newFiles = files.map {
if (file.fileId == it.fileId) it.copy(fileName = newName) else it
}
files = newFiles
}
}
}
paginationComponent {
this.paginationData = paginationData
Expand Down
70 changes: 62 additions & 8 deletions client/src/main/kotlin/components/FileComponent.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
package com.perpheads.files.components

import com.perpheads.files.data.FileResponse
import com.perpheads.files.data.validateFilename
import com.perpheads.files.showToast
import kotlinx.css.*
import kotlinx.html.InputType
import kotlinx.html.id
import kotlinx.html.js.onClickFunction
import react.RBuilder
import react.Props
import org.w3c.dom.HTMLInputElement
import react.*
import react.dom.*
import react.fc
import styled.*
import styled.css
import styled.styledI
import styled.styledImg

external interface FileComponentProps : Props {
var file: FileResponse
var deleteFile: (FileResponse) -> Unit
var renameFile: (FileResponse, String) -> Unit
}

val FileComponent = fc<FileComponentProps>("FileComponent") { props ->
val (editing, setEditing) = useState(false)
val extension = "." + props.file.fileName.split(".").last()

// Might be bad to do this for every component
useEffectOnce {
js("M.Dropdown.init(document.querySelectorAll(\".dropdown-trigger\"))")
Unit
}

val imgSrc = "/${props.file.fileId}/thumbnail"
tr {
td {
Expand All @@ -26,21 +41,60 @@ val FileComponent = fc<FileComponentProps>("FileComponent") { props ->
}
}
td {
a(href = "/${props.file.link}", target = "_blank") {
+props.file.fileName
if (editing) {
input(type = InputType.text) {
attrs.autoFocus = true
attrs.defaultValue = props.file.fileName.removeSuffix(extension)
attrs.onBlur = {
setEditing(false)
}
attrs.onKeyPress = { event ->
if (event.key == "Enter") {
val newName = (event.target as HTMLInputElement).value + extension
if (props.file.fileName != newName) {
if (validateFilename(newName)) {
props.renameFile(props.file, newName)
(event.target as HTMLInputElement).blur()
} else {
showToast("Invalid filename")
}
}
}
}
}
} else {
a(href = "/${props.file.link}", target = "_blank") {
+props.file.fileName
}
}
}
td { +props.file.formattedUploadDate }
td { +props.file.humanReadableByteSize() }
td {
a {
a(classes = "dropdown-trigger") {
attrs["data-target"] = "dropdown-${props.file.fileId}"
styledI {
css {
classes += "material-icons"
cursor = Cursor.pointer
}
+"more_horiz"
}
}

ul(classes = "dropdown-content") {
attrs.id = "dropdown-${props.file.fileId}"
li {
a {
attrs.onClickFunction = { props.deleteFile(props.file) }
+"delete"
+"Delete"
}
}
li {
a {
attrs.onClickFunction = { setEditing(true) }
+"Rename"
}
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions client/src/main/kotlin/components/FileListComponent.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package com.perpheads.files.components

import com.perpheads.files.data.FileResponse
import react.RBuilder
import react.Props
import react.StateSetter
import react.RBuilder
import react.dom.*
import react.fc

external interface FileListComponentProps : Props {
var files: List<FileResponse>
var deleteFile: (FileResponse) -> Unit
var renameFile: (FileResponse, String) -> Unit
}

val FileListComponent = fc<FileListComponentProps>("FileListComponent") { props ->
Expand All @@ -20,14 +20,15 @@ val FileListComponent = fc<FileListComponentProps>("FileListComponent") { props
th { +"Name" }
th { +"Date" }
th { +"Size" }
th { +"Delete" }
th { +"" }
}
}
tbody {
for (f in props.files) {
file {
file = f
deleteFile = props.deleteFile
renameFile = props.renameFile
}
}
}
Expand Down
30 changes: 23 additions & 7 deletions server/src/main/kotlin/controllers/FileController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,18 @@ package com.perpheads.files.controllers
import com.perpheads.files.*
import com.perpheads.files.NotFoundException
import com.perpheads.files.daos.FileDao
import com.perpheads.files.data.File
import com.perpheads.files.data.FileResponse
import com.perpheads.files.data.UploadResponse
import io.ktor.server.application.*
import io.ktor.server.plugins.BadRequestException
import com.perpheads.files.data.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.locations.*
import io.ktor.server.locations.post
import io.ktor.server.plugins.BadRequestException
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.routing.delete
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.toKotlinInstant
Expand Down Expand Up @@ -99,6 +95,9 @@ fun Route.fileRoutes(
}

suspend fun upload(userId: Int, filePart: PartData.FileItem): File {
filePart.originalFileName?.let {
if (!validateFilename(it)) { throw BadRequestException("Invalid filename") }
}
val randomFileName = secureRandom.alphaNumeric(16)
val link = randomFileName + getFileExtensionFromName(filePart.originalFileName ?: "")
val mimeType = (filePart.contentType ?: ContentType.Application.OctetStream).toString()
Expand Down Expand Up @@ -242,6 +241,23 @@ fun Route.fileRoutes(
)
}

post<FileRoute> { request ->
val newName = call.receive<RenameFileRequest>().newName
if (!validateFilename(newName)) {
throw BadRequestException("Invalid filename")
}
val file = withContext(Dispatchers.IO) {
fileDao.findByLink(request.link)
} ?: throw NotFoundException("File not found")
if (file.userId != call.user().userId) {
throw ForbiddenException("Not your file!")
}
withContext(Dispatchers.IO) {
fileDao.rename(file.fileId, newName)
}
call.respondText("")
}

delete<FileRoute> { request ->
val file = withContext(Dispatchers.IO) {
fileDao.findByLink(request.link)
Expand Down
11 changes: 11 additions & 0 deletions server/src/main/kotlin/daos/FileDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ class FileDao(conf: Configuration) {
.execute()
}

fun rename(
fileId: Int,
name: String,
create: DSLContext = dslContext
) {
create.update(FILES)
.set(FILES.FILE_NAME, name)
.where(FILES.FILE_ID.eq(fileId))
.execute()
}

fun getThumbnails(fileIds: List<Int>, userId: Int, create: DSLContext = dslContext): List<Pair<Int, ByteArray>> {
return create.select(FILES.FILE_ID, FILES.THUMBNAIL)
.from(FILES)
Expand Down
6 changes: 6 additions & 0 deletions shared/src/commonMain/kotlin/data/RenameFileRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.perpheads.files.data

import kotlinx.serialization.*

@Serializable
data class RenameFileRequest(val newName: String)
5 changes: 5 additions & 0 deletions shared/src/commonMain/kotlin/data/Util.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.perpheads.files.data

fun validateFilename(name: String): Boolean {
return name.matches("^[-_.A-Za-z0-9]+\$".toRegex())
}

0 comments on commit 9462351

Please sign in to comment.