diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f880791..4d224c8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -3,12 +3,14 @@ plugins {
// The shared code is located in `buildSrc/src/main/kotlin/kotlin-jvm.gradle.kts`.
id("buildsrc.convention.kotlin-jvm")
alias(libs.plugins.tempolin)
+ id("app.cash.sqldelight") version "2.0.2"
// Apply the Application plugin to add support for building an executable JVM application.
application
}
dependencies {
+ implementation("app.cash.sqldelight:sqlite-driver:2.0.2")
implementation("org.xerial:sqlite-jdbc:3.48.0.0")
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.+")
@@ -16,6 +18,7 @@ dependencies {
implementation(libs.javalin)
implementation(libs.commonsText)
implementation(project(":core"))
+ testImplementation(kotlin("test"))
}
application {
@@ -58,4 +61,12 @@ tempolin {
templateInterface = "net.h34t.filemure.Template"
}
-}
\ No newline at end of file
+}
+
+sqldelight {
+ databases {
+ create("Database") {
+ packageName.set("net.h34t.filemure.db")
+ }
+ }
+}
diff --git a/app/src/main/kotlin/net/h34t/filemure/DateGuesser.kt b/app/src/main/kotlin/net/h34t/filemure/DateGuesser.kt
index eebfdae..2e5fc0b 100644
--- a/app/src/main/kotlin/net/h34t/filemure/DateGuesser.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/DateGuesser.kt
@@ -3,6 +3,7 @@ package net.h34t.filemure
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeParseException
object DateGuesser {
@@ -27,8 +28,8 @@ object DateGuesser {
Regex("\\d{4}\\d{2}\\d{2} \\d{2}\\d{2}\\d{2}") to DateTimeFormatter.ofPattern("yyyyMMdd' 'HHmmss"),
Regex("\\d{4}\\d{2}\\d{2} \\d{2}\\d{2}") to DateTimeFormatter.ofPattern("yyyyMMdd' 'HHmm"),
- Regex("\\d{4}\\d{2}\\d{2}\\d{2}\\d{2}\\d{2}") to DateTimeFormatter.ofPattern("yyyyMMdd' 'HHmmss"),
- Regex("\\d{4}\\d{2}\\d{2}\\d{2}\\d{2}") to DateTimeFormatter.ofPattern("yyyyMMdd' 'HHmm"),
+ Regex("\\d{4}\\d{2}\\d{2}\\d{2}\\d{2}\\d{2}") to DateTimeFormatter.ofPattern("yyyyMMddHHmmss"),
+ Regex("\\d{4}\\d{2}\\d{2}\\d{2}\\d{2}") to DateTimeFormatter.ofPattern("yyyyMMddHHmm"),
)
private val datePatterns = listOf(
@@ -40,12 +41,24 @@ object DateGuesser {
)
- fun guessDateTime(filename: String) = dateTimePatterns.asSequence().mapNotNull {
- it.first.find(filename)?.let { mr -> LocalDateTime.parse(mr.groups[0]?.value!!, it.second) }
+ private fun guessDateTime(filename: String) = dateTimePatterns.asSequence().map {
+ it.first.findAll(filename).mapNotNull { mr ->
+ try {
+ LocalDateTime.parse(mr.groups[0]?.value!!, it.second)
+ } catch (e: DateTimeParseException) {
+ null
+ }
+ }.firstOrNull()
}.firstOrNull()
- fun guessDate(filename: String) = datePatterns.asSequence().mapNotNull {
- it.first.find(filename)?.let { mr -> LocalDate.parse(mr.groups[0]?.value!!, it.second) }
+ private fun guessDate(filename: String) = datePatterns.asSequence().map {
+ it.first.findAll(filename).mapNotNull { mr ->
+ try {
+ LocalDate.parse(mr.groups[0]?.value!!, it.second)
+ } catch (e: DateTimeParseException) {
+ null
+ }
+ }.firstOrNull()
}.firstOrNull()
fun guess(filename: String): LocalDateTime? {
diff --git a/app/src/main/kotlin/net/h34t/filemure/Entities.kt b/app/src/main/kotlin/net/h34t/filemure/Entities.kt
deleted file mode 100644
index 939b66c..0000000
--- a/app/src/main/kotlin/net/h34t/filemure/Entities.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.h34t.filemure
-
-import java.time.LocalDateTime
-
-data class LimboFile(
- val id: Long,
- val accountId: Long,
- val filename: String,
- val contentType: String,
- val size: Long,
- val created: LocalDateTime,
- )
\ No newline at end of file
diff --git a/app/src/main/kotlin/net/h34t/filemure/FilemureApp.kt b/app/src/main/kotlin/net/h34t/filemure/FilemureApp.kt
index 0c7cd63..0d7b70e 100644
--- a/app/src/main/kotlin/net/h34t/filemure/FilemureApp.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/FilemureApp.kt
@@ -20,8 +20,9 @@ class FilemureApp(repository: SqliteRepository) {
fun register(server: Javalin) {
server.beforeMatched { ctx ->
+ println(ctx.req().pathInfo)
val userRole = getUserRole(ctx)
- if (!ctx.routeRoles().contains(userRole)) {
+ if (ctx.routeRoles().isNotEmpty() && !ctx.routeRoles().contains(userRole)) {
ctx.redirectPRG("/login")
throw UnauthorizedResponse()
}
@@ -38,7 +39,11 @@ class FilemureApp(repository: SqliteRepository) {
server.get("/document/new", documentController::createDocumentForm, Role.USER)
server.post("/document/new", documentController::createDocument, Role.USER)
server.get("/document/{extId}", documentController::documentDetail, Role.USER)
+ server.get("/document/{extId}/download", documentController::downloadDocument, Role.USER)
+ server.get("/document/{extId}/edit", documentController::editDocumentForm, Role.USER)
+ server.post("/document/{extId}/edit", documentController::editDocumentAction, Role.USER)
+ server.get("/file/{extId}/download", documentController::downloadFile, Role.USER)
server.exception(UnauthorizedResponse::class.java) { e, ctx ->
ctx.tempolin(
diff --git a/app/src/main/kotlin/net/h34t/filemure/Server.kt b/app/src/main/kotlin/net/h34t/filemure/Server.kt
index 3db3f73..216745c 100644
--- a/app/src/main/kotlin/net/h34t/filemure/Server.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/Server.kt
@@ -18,6 +18,7 @@ fun main() {
staticFiles.hostedPath = "/"
staticFiles.directory = "./public"
staticFiles.location = Location.EXTERNAL
+ // staticFiles.
}
config.requestLogger.http { ctx, ms ->
diff --git a/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt b/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt
index 08f9dcf..3ac608c 100644
--- a/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt
@@ -4,8 +4,8 @@ import net.h34t.filemure.tpl.*
import org.apache.commons.text.StringEscapeUtils
import java.net.URLEncoder
-class TemplateModifiers : Frame.Modifiers, Limbo.Modifiers, NewDocumentForm.Modifiers, Overview.Modifiers,
- Document.Modifiers {
+class TemplateModifiers : Frame.Modifiers, Limbo.Modifiers, DocumentCreateForm.Modifiers, Overview.Modifiers,
+ Document.Modifiers, FilePreview.Modifiers, DocumentEditForm.Modifiers, FileList.Modifiers {
fun hashPrefix(arg: String): String {
return URLEncoder.encode(arg, Charsets.UTF_8)
diff --git a/app/src/main/kotlin/net/h34t/filemure/Util.kt b/app/src/main/kotlin/net/h34t/filemure/Util.kt
index 021956e..0d5c10e 100644
--- a/app/src/main/kotlin/net/h34t/filemure/Util.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/Util.kt
@@ -48,4 +48,12 @@ value class ExtId(val value: String) {
return ExtId((0..8).map { chars.random() }.joinToString(""))
}
}
-}
\ No newline at end of file
+}
+
+fun formatHumanReadableSize(bytes: Long) = when (bytes) {
+ in 0L..<1024L -> "$bytes bytes"
+ in 1025..<1024 * 1_000 -> "${bytes / 1000} kb"
+ in 1025 * 1_000..<1024 * 1_000_000 -> "${bytes / 1_000_000} mb"
+ else -> "${bytes / 1_000_000_000} gb"
+}
+
diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt
index f228f5e..fc57d96 100644
--- a/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/controller/DocumentController.kt
@@ -3,11 +3,11 @@ package net.h34t.filemure.controller
import io.javalin.http.BadRequestResponse
import io.javalin.http.Context
import io.javalin.http.ForbiddenResponse
+import io.javalin.http.Header
import net.h34t.filemure.*
+import net.h34t.filemure.core.entity.Tag
import net.h34t.filemure.repository.SqliteRepository
-import net.h34t.filemure.tpl.Document
-import net.h34t.filemure.tpl.Frame
-import net.h34t.filemure.tpl.NewDocumentForm
+import net.h34t.filemure.tpl.*
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@@ -16,7 +16,7 @@ import java.time.format.FormatStyle
class DocumentController(val modifiers: TemplateModifiers, val repository: SqliteRepository) {
private val dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)
- private val isoDtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
+ private val formDtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
fun documentDetail(ctx: Context) {
val session = ctx.requireSession()
@@ -31,19 +31,25 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
isTarget = true,
content = Document(
modifiers = modifiers,
+ extId = document.extId,
title = document.title,
referenceDate = dtf.format(document.referenceDate),
tags = { document.tags.map { TagsBlock(tag = it.value) }.asSequence() },
description = document.description,
- files = {
- document.files.map { file ->
- FilesBlock(
- extId = file.extId,
- filename = file.filename,
- contentType = file.contentType ?: "?"
- )
- }.asSequence()
- },
+ files = FileList(
+ modifiers = modifiers,
+ delete = true,
+ files = {
+ document.files.map { file ->
+ FilesBlock(
+ extId = file.extId,
+ filename = file.filename,
+ contentType = file.contentType ?: "?",
+ size = formatHumanReadableSize(file.fileSize),
+ delete = true,
+ )
+ }.asSequence()
+ }),
)
)
)
@@ -53,42 +59,51 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
fun createDocumentForm(ctx: Context) {
val session = ctx.requireSession()
- val fileIds = ctx.queryParams("file_id").map { it.toLong() }
+ val fileIds = ctx.queryParams("file_id")
val limboFiles = repository.getFilesInLimbo(session.id)
- val limboFileIds = limboFiles.map { it.id }
+ val limboFileIds = limboFiles.map { it.extId }
if (!fileIds.all { it in limboFileIds }) {
throw ForbiddenResponse("Mismatched file ids.")
}
- val selectedFiles = limboFiles.filter { it.id in fileIds }
+ val selectedFiles = limboFiles.filter { it.extId in fileIds }
- val title = "new document"
- val referenceDate = selectedFiles.asSequence().mapNotNull { DateGuesser.guess(it.filename) }.firstOrNull()
+ val title = selectedFiles.firstOrNull()?.filename ?: "new document"
+ val referenceDates = selectedFiles.mapNotNull { DateGuesser.guess(it.filename) }
+
+ val referenceDate = referenceDates.firstOrNull()
?: LocalDateTime.now()
val tags = selectedFiles.map { File(it.filename).extension }.distinct().asSequence()
val description = ""
ctx.tempolin(
Frame(
- modifiers = modifiers, title = "new document", isTarget = true, content = NewDocumentForm(
+ modifiers = modifiers,
+ title = "new document",
+ isTarget = true,
+ content = DocumentCreateForm(
modifiers = modifiers,
title = title,
- referenceDate = isoDtf.format(referenceDate),
+ referenceDate = formDtf.format(referenceDate),
tags = { tags.map { TagsBlock(it) } },
description = description,
- files = {
- selectedFiles
- .map {
+ files = FileList(
+ modifiers = modifiers,
+ delete = true,
+ files = {
+ selectedFiles.map { file ->
FilesBlock(
- id = it.id.toString(),
- filename = it.filename,
- contentType = it.contentType
+ extId = file.extId,
+ filename = file.filename,
+ contentType = file.contentType ?: "?",
+ size = formatHumanReadableSize(file.fileSize),
+ delete = true,
)
- }
- .asSequence()
- })
+ }.asSequence()
+ }),
+ )
)
)
}
@@ -98,15 +113,92 @@ class DocumentController(val modifiers: TemplateModifiers, val repository: Sqlit
val title = ctx.formParam("title")
?: throw BadRequestResponse("")
- val referenceDate = ctx.formParam("reference_date")?.let { LocalDateTime.parse(it, isoDtf) }
+ val referenceDate = ctx.formParam("reference_date")?.let { LocalDateTime.parse(it, formDtf) }
?: throw BadRequestResponse("")
val tags = ctx.formParam("tags")?.split("\\s+")
?: emptyList()
val description = ctx.formParam("description")
?: throw BadRequestResponse("")
- val fileIds = ctx.formParams("file_id").map { it.toLong() }
+ val fileExtIds = ctx.formParams("file_id")
- val extId = repository.addDocument(session.id, title, referenceDate, tags, description, fileIds)
+ val extId = repository.addDocument(session.id, title, referenceDate, tags, description, fileExtIds)
+
+ ctx.redirectPRG("/document/$extId")
+ }
+
+ fun downloadDocument(ctx: Context) {
+ // TODO pack all files in a zip
+ }
+
+ fun downloadFile(ctx: Context) {
+ val session = ctx.requireSession()
+ val extId = ctx.pathParam("extId")
+
+ val file = repository.loadFile(accountId = session.id, extId = extId)
+
+ file.contentType?.also { ctx.header(Header.CONTENT_TYPE, it) }
+ ctx.result(file.content)
+ }
+
+ fun editDocumentForm(ctx: Context) {
+ val session = ctx.requireSession()
+ val extId = ctx.pathParam("extId")
+
+ val document = repository.getDocumentByExtId(accountId = session.id, extId = extId)
+
+ ctx.tempolin(
+ Frame(
+ modifiers = modifiers,
+ title = document.title,
+ isTarget = false,
+ content = DocumentEditForm(
+ modifiers = modifiers,
+ extId = extId,
+ title = document.title,
+ referenceDate = formDtf.format(document.referenceDate),
+ tags = {
+ document.tags.map { tag -> TagsBlock(tag = tag.value) }.asSequence()
+ },
+ description = document.description,
+ files = FileList(
+ modifiers = modifiers,
+ delete = true,
+ files = {
+ document.files.map { file ->
+ FilesBlock(
+ extId = file.extId,
+ filename = file.filename,
+ contentType = file.contentType ?: "?",
+ size = formatHumanReadableSize(file.fileSize),
+ delete = true,
+ )
+ }.asSequence()
+ }),
+ )
+ )
+ )
+ }
+
+ fun editDocumentAction(ctx: Context) {
+ val session = ctx.requireSession()
+ val extId = ctx.pathParam("extId")
+
+ val document = repository.getDocumentByExtId(accountId = session.id, extId = extId)
+
+
+ val title = ctx.formParam("title")
+ val referenceDate = ctx.formParam("reference_date")
+ val description = ctx.formParam("description") ?: ""
+ val tags = ctx.formParam("tags")
+
+ repository.updateDocument(
+ accountId = session.id,
+ id = document.id,
+ title = title ?: "",
+ referenceDate = LocalDateTime.parse(referenceDate ?: "", formDtf),
+ tags = Tag.parse(tags),
+ description = description
+ )
ctx.redirectPRG("/document/$extId")
}
diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt
index 7828a11..5ede23c 100644
--- a/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt
@@ -26,10 +26,10 @@ class LimboController(val modifiers: TemplateModifiers, val repository: SqliteRe
modifiers = modifiers, limboFileCount = files.size.toString(), file = {
files.map { f ->
FileBlock(
- id = f.id.toString(),
+ extId = f.extId,
file = f.filename,
- type = f.contentType,
- size = f.size.toString(),
+ type = f.contentType ?: "",
+ size = f.fileSize.toString(),
uploaded = f.created.format(dtf)
)
}.asSequence()
diff --git a/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt b/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt
index 11118b6..419dcb7 100644
--- a/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt
+++ b/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt
@@ -1,10 +1,8 @@
package net.h34t.filemure.repository
import net.h34t.filemure.ExtId
-import net.h34t.filemure.LimboFile
-import net.h34t.filemure.core.entity.Document
-import net.h34t.filemure.core.entity.FileRef
-import net.h34t.filemure.core.entity.Tag
+import net.h34t.filemure.core.entity.*
+import net.h34t.filemure.core.entity.Tag.Companion.serialize
import java.io.InputStream
import java.sql.Connection
import java.sql.DriverManager
@@ -33,37 +31,63 @@ class SqliteRepository(url: String) {
}
}
- fun getLimboFileCount(accountId: Long): Long {
- connection.prepareStatement("SELECT count(*) AS count FROM file WHERE account_id=? AND document_id IS NULL")
+ fun getLimboFileCount(accountId: Long, state: State = State.ACTIVE): Long {
+ connection.prepareStatement("SELECT count(*) AS count FROM file WHERE account_id=? AND document_id IS NULL AND state=?")
.use { stmt ->
stmt.setLong(1, accountId)
+ stmt.setInt(2, state.code)
val rs = stmt.executeQuery()
rs.next()
return rs.getLong(1)
}
}
- fun getFilesInLimbo(accountId: Long): List
Filename | +Type | +Size | +Download | + {if $delete} +Delete | + {/if} +
---|---|---|---|---|
{*$filename} | +{*$contentType} | +{*$size} | +download | + {if $delete} +delete | + {/if} +