From 69a43e62521b184b4e9cf3e6d23292837ee49e72 Mon Sep 17 00:00:00 2001
From: Stefan Schallerl
Date: Wed, 5 Feb 2025 17:14:45 +0100
Subject: [PATCH] Intermittent commit * Adds document editing * Adds human
readable byte size formatting * Adds file downloading * Uses extIds instead
of ids in some places.
---
app/build.gradle.kts | 13 +-
.../kotlin/net/h34t/filemure/DateGuesser.kt | 25 +-
.../main/kotlin/net/h34t/filemure/Entities.kt | 12 -
.../kotlin/net/h34t/filemure/FilemureApp.kt | 7 +-
.../main/kotlin/net/h34t/filemure/Server.kt | 1 +
.../net/h34t/filemure/TemplateModifiers.kt | 4 +-
app/src/main/kotlin/net/h34t/filemure/Util.kt | 10 +-
.../filemure/controller/DocumentController.kt | 156 +++++--
.../filemure/controller/LimboController.kt | 6 +-
.../filemure/repository/SqliteRepository.kt | 379 ++++++++++--------
.../create_db.sql => sqldelight/database.sql} | 16 +-
.../net.h34t.filemure.tpl/Document.tpl.html | 8 +-
...m.tpl.html => DocumentCreateForm.tpl.html} | 10 +-
.../DocumentEditForm.tpl.html | 20 +
.../net.h34t.filemure.tpl/FileList.tpl.html | 24 ++
.../FilePreview.tpl.html | 9 +
.../tpl/net.h34t.filemure.tpl/Limbo.tpl.html | 2 +-
.../net/h34t/filemure/DateGuesserTest.kt | 36 ++
.../net/h34t/filemure/core/entity/Types.kt | 22 +
public/filemure.css | 15 +
20 files changed, 540 insertions(+), 235 deletions(-)
delete mode 100644 app/src/main/kotlin/net/h34t/filemure/Entities.kt
rename app/src/main/{resources/create_db.sql => sqldelight/database.sql} (73%)
rename app/src/main/tpl/net.h34t.filemure.tpl/{NewDocumentForm.tpl.html => DocumentCreateForm.tpl.html} (78%)
create mode 100644 app/src/main/tpl/net.h34t.filemure.tpl/DocumentEditForm.tpl.html
create mode 100644 app/src/main/tpl/net.h34t.filemure.tpl/FileList.tpl.html
create mode 100644 app/src/main/tpl/net.h34t.filemure.tpl/FilePreview.tpl.html
create mode 100644 app/src/test/kotlin/net/h34t/filemure/DateGuesserTest.kt
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 {
- connection.prepareStatement("SELECT id, account_id, filename, content_type, file_size, created FROM file WHERE account_id=? AND document_id IS NULL ORDER BY created DESC")
- .use { stmt ->
- stmt.setLong(1, accountId)
- val res = stmt.executeQuery()
+ fun getFilesInLimbo(accountId: Long, state: State = State.ACTIVE): List {
+ connection.prepareStatement(
+ """
+ |SELECT
+ | id,
+ | ext_id,
+ | account_id,
+ | filename,
+ | content_type,
+ | content_extracted,
+ | file_size,
+ | created,
+ | state
+ |FROM
+ | file
+ |WHERE
+ | account_id=? AND
+ | document_id IS NULL
+ | AND state=?
+ |ORDER BY
+ | created DESC
+ """.trimMargin()
+ ).use { stmt ->
+ stmt.setLong(1, accountId)
+ stmt.setInt(2, state.code)
+ val res = stmt.executeQuery()
- val list = mutableListOf()
+ val list = mutableListOf()
- while (res.next()) {
- list += LimboFile(
- id = res.getLong(1),
- accountId = res.getLong(2),
- filename = res.getString(3),
- contentType = res.getString(4),
- size = res.getLong(5),
- created = LocalDateTime.parse(res.getString(6), dtf)
- )
- }
-
- return list
+ while (res.next()) {
+ list += FileRef(
+ id = res.getLong("id"),
+ extId = res.getString("ext_id"),
+ accountId = res.getLong("account_id"),
+ documentId = null,
+ filename = res.getString("filename"),
+ contentType = res.getString("content_type"),
+ contentExtracted = res.getString("content_extracted"),
+ fileSize = res.getLong("file_size"),
+ created = LocalDateTime.parse(res.getString("created"), dtf),
+ state = State.fromCode(res.getInt("state"))
+ )
}
+
+ return list
+ }
}
fun addDocument(
@@ -72,7 +96,7 @@ class SqliteRepository(url: String) {
referenceDate: LocalDateTime,
tags: List,
description: String,
- fileIds: List
+ fileExtIds: List
): ExtId {
val savePoint = connection.setSavepoint()
@@ -97,14 +121,14 @@ class SqliteRepository(url: String) {
gks.getLong(1)
}
- val ids = fileIds.joinToString(",")
+ val extIds = fileExtIds.joinToString(",") { "'$it'" }
- connection.prepareStatement("""UPDATE file SET document_id=? WHERE account_id=? AND id IN ($ids)""")
+ connection.prepareStatement("""UPDATE file SET document_id=? WHERE account_id=? AND ext_id IN ($extIds)""")
.use { stmt ->
stmt.setLong(1, documentId)
stmt.setLong(2, accountId)
val affected = stmt.executeUpdate()
- require(affected == fileIds.size)
+ require(affected == fileExtIds.size)
}
connection.commit()
@@ -115,131 +139,124 @@ class SqliteRepository(url: String) {
}
}
- fun getDocuments(accountId: Long): List {
- connection.prepareStatement("""SELECT d.id, d.account_id, d.ext_id, d.title, d.description, d.tags, d.created, d.reference_date FROM document d WHERE account_id=?""")
- .use { stmt ->
- stmt.setLong(1, accountId)
- val res = stmt.executeQuery()
+ fun getDocuments(accountId: Long, state: State = State.ACTIVE): List {
+ connection.prepareStatement(
+ """
+ |SELECT
+ | d.id,
+ | d.account_id,
+ | d.ext_id,
+ | d.title,
+ | d.description,
+ | d.tags,
+ | d.created,
+ | d.reference_date,
+ | d.state
+ |FROM
+ | document d
+ |WHERE
+ | account_id=? AND
+ | state=?
+ """.trimMargin()
+ ).use { stmt ->
+ stmt.setLong(1, accountId)
+ stmt.setInt(2, state.code)
+ val res = stmt.executeQuery()
- val documentList = mutableListOf()
+ val documentList = mutableListOf()
- while (res.next()) {
- documentList.add(
- Document(
- id = res.getLong(1),
- extId = res.getString(3),
- title = res.getString(4),
- description = res.getString(5),
- tags = res.getString(6)
- ?.let { if (it.isNotBlank()) it.split(",").map { tag -> Tag(tag) } else emptyList() }
- ?: emptyList(),
- created = LocalDateTime.parse(res.getString(7), dtf),
- referenceDate = LocalDateTime.parse(res.getString(8), dtf),
- files = emptyList()
- )
+ while (res.next()) {
+ documentList.add(
+ Document(
+ id = res.getLong("id"),
+ extId = res.getString("ext_id"),
+ title = res.getString("title"),
+ description = res.getString("description"),
+ tags = res.getString("tags")
+ ?.let { if (it.isNotBlank()) it.split(",").map { tag -> Tag(tag) } else emptyList() }
+ ?: emptyList(),
+ created = LocalDateTime.parse(res.getString("created"), dtf),
+ referenceDate = LocalDateTime.parse(res.getString("reference_date"), dtf),
+ state = State.fromCode(res.getInt("state")),
+ files = emptyList()
)
- }
-
-
- return documentList.toList()
+ )
}
+
+
+ return documentList.toList()
+ }
}
- fun getDocumentById(accountId: Long, id: Long): Document {
- return connection.prepareStatement(
+ fun updateDocument(
+ accountId: Long,
+ id: Long,
+ title: String,
+ referenceDate: LocalDateTime,
+ tags: List,
+ description: String,
+ state: State = State.ACTIVE,
+ ) {
+ connection.prepareStatement(
"""
- SELECT
- d.id,
- d.account_id,
- d.ext_id,
- d.title,
- d.description,
- d.tags,
- d.created,
- d.reference_date,
- f.id,
- f.ext_id,
- f.filename,
- f.content_type,
- f.content_extracted,
- f.file_size
- f.created
- FROM
- document d LEFT OUTER JOIN file f ON (d.id = f.document_id)
- WHERE
- id=? AND
- account_id=?
- """.trimIndent()
- )
- .use { stmt ->
- stmt.setLong(1, id)
- stmt.setLong(2, accountId)
- val res = stmt.executeQuery()
+ |UPDATE
+ | document
+ |SET
+ | title=?,
+ | reference_date=?,
+ | tags=?,
+ | description=?,
+ | state=?
+ |WHERE
+ | account_id=? AND
+ | id=?
+ """.trimMargin()
+ ).use { stmt ->
+ stmt.setString(1, title)
+ stmt.setString(2, dtf.format(referenceDate))
+ stmt.setString(3, tags.serialize())
+ stmt.setString(4, description)
+ stmt.setInt(5, state.code)
+ stmt.setLong(6, accountId)
+ stmt.setLong(7, id)
- var document: Document? = null
- val files = mutableListOf()
-
- while (res.next()) {
- if (document == null) {
- document = Document(
- id = res.getLong(1),
- extId = res.getString(3),
- title = res.getString(4),
- description = res.getString(5),
- tags = res.getString(6).split(",").map { Tag(it) },
- created = LocalDateTime.parse(res.getString(7), dtf),
- referenceDate = LocalDateTime.parse(res.getString(8), dtf),
- files = emptyList()
- )
-
- val id = res.getLong(9)
- if (!res.wasNull()) {
- files.add(
- FileRef(
- id = id,
- extId = res.getString(10),
- filename = res.getString(11),
- contentType = res.getString(12),
- contentExtracted = res.getString(13),
- fileSize = res.getLong(14),
- created = LocalDateTime.parse(res.getString(15), dtf)
- )
- )
- }
- }
- }
-
- document?.copy(files = files) ?: throw IllegalStateException("Document is null")
- }
+ stmt.executeUpdate()
+ }
}
fun getDocumentByExtId(accountId: Long, extId: String): Document {
return connection.prepareStatement(
"""
- SELECT
- d.id,
- d.account_id,
- d.ext_id,
- d.title,
- d.description,
- d.tags,
- d.created,
- d.reference_date,
- f.id,
- f.ext_id,
- f.filename,
- f.content_type,
- f.content_extracted,
- f.file_size,
- f.created
- FROM
- document d LEFT OUTER JOIN file f ON (d.id = f.document_id)
- WHERE
- d.ext_id=? AND
- d.account_id=?
- """.trimIndent()
+ |SELECT
+ | d.id as d_id,
+ | d.account_id d_account_id,
+ | d.ext_id d_ext_id,
+ | d.title d_title,
+ | d.description d_description,
+ | d.tags d_tags,
+ | d.created d_created,
+ | d.reference_date d_reference_date,
+ | d.state as d_state,
+ | f.id f_id,
+ | f.ext_id f_ext_id,
+ | f.account_id f_account_id,
+ | f.document_id f_document_id,
+ | f.filename f_filename,
+ | f.content_type f_content_type,
+ | f.content_extracted f_content_extracted,
+ | f.file_size f_file_size,
+ | f.created f_created,
+ | f.state f_state
+ |FROM
+ | document d LEFT OUTER JOIN file f ON (d.id = f.document_id)
+ |WHERE
+ | d.ext_id=? AND
+ | d.account_id=?
+ """.trimMargin()
)
.use { stmt ->
+
+
stmt.setString(1, extId)
stmt.setLong(2, accountId)
val res = stmt.executeQuery()
@@ -249,36 +266,76 @@ class SqliteRepository(url: String) {
while (res.next()) {
if (document == null) {
document = Document(
- id = res.getLong(1),
- extId = res.getString(3),
- title = res.getString(4),
- description = res.getString(5),
- tags = Tag.parse(res.getString(6)),
- created = LocalDateTime.parse(res.getString(7), dtf),
- referenceDate = LocalDateTime.parse(res.getString(8), dtf),
+ id = res.getLong("d_id"),
+ extId = res.getString("d_ext_id"),
+ title = res.getString("d_title"),
+ description = res.getString("d_description"),
+ tags = Tag.parse(res.getString("d_tags")),
+ created = LocalDateTime.parse(res.getString("d_created"), dtf),
+ referenceDate = LocalDateTime.parse(res.getString("d_reference_date"), dtf),
+ state = State.fromCode(res.getInt("d_state")),
files = emptyList()
)
-
- val fid: Long = res.getLong(9)
-
- if (!res.wasNull()) {
- files.add(
- FileRef(
- id = fid,
- extId = res.getString(10),
- filename = res.getString(11),
- contentType = res.getString(12),
- contentExtracted = res.getString(13),
- fileSize = res.getLong(14),
- created = LocalDateTime.parse(res.getString(15), dtf)
- )
- )
- }
}
+
+ val fid: Long = res.getLong("f_id")
+ val wasNull = res.wasNull()
+
+ if (!wasNull) {
+ files.add(
+ FileRef(
+ id = fid,
+ extId = res.getString("f_ext_id"),
+ accountId = res.getLong("f_account_id"),
+ documentId = res.getLong("f_document_id"),
+ filename = res.getString("f_filename"),
+ contentType = res.getString("f_content_type"),
+ contentExtracted = res.getString("f_content_extracted"),
+ fileSize = res.getLong("f_file_size"),
+ created = LocalDateTime.parse(res.getString("f_created"), dtf),
+ state = State.fromCode(res.getInt("f_state"))
+ )
+ )
+ }
+
}
requireNotNull(document)
document.copy(files = files)
}
}
+
+ fun loadFile(accountId: Long, extId: String): FileContent {
+ return connection.prepareStatement(
+ """
+ |SELECT
+ | id,
+ | ext_id,
+ | filename,
+ | content_type,
+ | content_extracted,
+ | file_size,
+ | content
+ |FROM
+ | file
+ |WHERE
+ | account_id=? AND
+ | ext_id=?
+ """.trimMargin()
+ ).use { stmt ->
+ stmt.setLong(1, accountId)
+ stmt.setString(2, extId)
+ val res = stmt.executeQuery()
+
+ FileContent(
+ id = res.getLong(1),
+ extId = res.getString(2),
+ filename = res.getString(3),
+ contentType = res.getString(4),
+ contentExtracted = res.getString(5),
+ fileSize = res.getLong(6),
+ content = res.getBytes(7)
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/resources/create_db.sql b/app/src/main/sqldelight/database.sql
similarity index 73%
rename from app/src/main/resources/create_db.sql
rename to app/src/main/sqldelight/database.sql
index 8c9022c..5de5c0c 100644
--- a/app/src/main/resources/create_db.sql
+++ b/app/src/main/sqldelight/database.sql
@@ -4,10 +4,14 @@ CREATE TABLE account (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL,
- created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ created TEXT DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
+ state INTEGER DEFAULT (1) NOT NULL,
unique(email)
);
+CREATE INDEX account_state_IDX ON account (state);
+
+
-- document definition
CREATE TABLE document (
@@ -18,12 +22,16 @@ CREATE TABLE document (
description TEXT NOT NULL,
tags TEXT NOT NULL,
created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
- reference_date TEXT,
+ reference_date TEXT, state INTEGER DEFAULT (1) NOT NULL,
CONSTRAINT document_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE,
unique(ext_id)
);
+CREATE INDEX document_state_IDX ON document (state);
+
+
-- file definition
+
CREATE TABLE file (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
@@ -34,8 +42,10 @@ CREATE TABLE file (
content BLOB NOT NULL,
content_type TEXT,
content_extracted TEXT,
- created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, state INTEGER DEFAULT (1) NOT NULL,
CONSTRAINT file_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE,
CONSTRAINT file_document_FK FOREIGN KEY (document_id) REFERENCES document(id) ON DELETE CASCADE,
unique(ext_id)
);
+
+CREATE INDEX file_state_IDX ON file (state);
\ No newline at end of file
diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/Document.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/Document.tpl.html
index 1a09e32..db6ef1c 100644
--- a/app/src/main/tpl/net.h34t.filemure.tpl/Document.tpl.html
+++ b/app/src/main/tpl/net.h34t.filemure.tpl/Document.tpl.html
@@ -8,8 +8,6 @@
{*$description}
-
+{template $files}
+
+edit
\ No newline at end of file
diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/NewDocumentForm.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/DocumentCreateForm.tpl.html
similarity index 78%
rename from app/src/main/tpl/net.h34t.filemure.tpl/NewDocumentForm.tpl.html
rename to app/src/main/tpl/net.h34t.filemure.tpl/DocumentCreateForm.tpl.html
index 2727dac..b5735fa 100644
--- a/app/src/main/tpl/net.h34t.filemure.tpl/NewDocumentForm.tpl.html
+++ b/app/src/main/tpl/net.h34t.filemure.tpl/DocumentCreateForm.tpl.html
@@ -13,12 +13,8 @@
-
- {for $files}
- {*$filename} ({*$contentType})
-
- {/for}
-
-
+
+ {template $files}
+
\ No newline at end of file
diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/DocumentEditForm.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/DocumentEditForm.tpl.html
new file mode 100644
index 0000000..2342f51
--- /dev/null
+++ b/app/src/main/tpl/net.h34t.filemure.tpl/DocumentEditForm.tpl.html
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/FileList.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/FileList.tpl.html
new file mode 100644
index 0000000..1d5dd7d
--- /dev/null
+++ b/app/src/main/tpl/net.h34t.filemure.tpl/FileList.tpl.html
@@ -0,0 +1,24 @@
+
+
+ Filename
+ Type
+ Size
+ Download
+ {if $delete}
+ Delete
+ {/if}
+
+
+ {for $files}
+
+
+ {*$filename}
+ {*$contentType}
+ {*$size}
+ download
+ {if $delete}
+ delete
+ {/if}
+
+ {/for}
+
\ No newline at end of file
diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/FilePreview.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/FilePreview.tpl.html
new file mode 100644
index 0000000..a015085
--- /dev/null
+++ b/app/src/main/tpl/net.h34t.filemure.tpl/FilePreview.tpl.html
@@ -0,0 +1,9 @@
+
+ {if $embed}
+
+ {/if}
+ {if $image}
+
+ {/if}
+
download
+
\ No newline at end of file
diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html
index 3acd956..236777d 100644
--- a/app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html
+++ b/app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html
@@ -12,7 +12,7 @@
{for $file}
-
+
{*$file}
{*$type}
{*$size}
diff --git a/app/src/test/kotlin/net/h34t/filemure/DateGuesserTest.kt b/app/src/test/kotlin/net/h34t/filemure/DateGuesserTest.kt
new file mode 100644
index 0000000..6585fec
--- /dev/null
+++ b/app/src/test/kotlin/net/h34t/filemure/DateGuesserTest.kt
@@ -0,0 +1,36 @@
+package net.h34t.filemure
+
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class DateGuesserTest {
+
+ private val dtf = DateTimeFormatter.ISO_DATE_TIME
+
+ private fun toDateTime(value: String?): LocalDateTime? = value?.let { LocalDateTime.parse(value, dtf) }
+
+ @Test
+ fun test_date() {
+ val values = listOf(
+ "2023-10-05T00:00" to "20231005_Form.pdf",
+ "2023-10-05T10:15" to "File-20231005-101500_Form.pdf",
+ "2023-10-05T00:00" to "File-2023-10-05_Form.pdf",
+ "2023-10-05T00:00" to "File-2023_10_05_Form.pdf",
+ "2023-10-05T00:00" to "File-2023_10_05.pdf",
+ "2023-10-05T11:22:33" to "File.20231005-112233.pdf",
+ "2022-11-11T00:00:00" to "File.20221111000000.pdf",
+ "8888-11-11T00:00:00" to "File.88881111000000.pdf",
+ null to "Document.pdf",
+ )
+
+ values.forEach { value ->
+ assertEquals(
+ toDateTime(value.first),
+ DateGuesser.guess(value.second),
+ "Guessing the date failed for ${value.first} -> ${value.second}"
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/h34t/filemure/core/entity/Types.kt b/core/src/main/kotlin/net/h34t/filemure/core/entity/Types.kt
index fae081d..22c36b5 100644
--- a/core/src/main/kotlin/net/h34t/filemure/core/entity/Types.kt
+++ b/core/src/main/kotlin/net/h34t/filemure/core/entity/Types.kt
@@ -10,6 +10,7 @@ data class Document(
val tags: List,
val created: LocalDateTime,
val referenceDate: LocalDateTime,
+ val state: State,
val files: List,
)
@@ -32,16 +33,23 @@ value class Tag(val value: String) {
data class FileRef(
val id: Long,
val extId: String,
+ val accountId: Long,
+ val documentId: Long?,
val filename: String,
val contentType: String?,
val contentExtracted: String?,
val fileSize: Long,
val created: LocalDateTime,
+ val state: State
)
data class FileContent(
val id: Long,
val extId: String,
+ val filename: String,
+ val contentType: String?,
+ val contentExtracted: String?,
+ val fileSize: Long,
val content: ByteArray,
) {
override fun equals(other: Any?): Boolean {
@@ -65,3 +73,17 @@ data class FileContent(
}
}
+enum class State(val code: Int) {
+ ACTIVE(1),
+ ARCHIVED(2),
+ DELETED(3);
+
+ companion object {
+ fun fromCode(code: Int) = when (code) {
+ 1 -> ACTIVE
+ 2 -> ARCHIVED
+ 3 -> DELETED
+ else -> throw IllegalArgumentException("$code doesn't exist.")
+ }
+ }
+}
diff --git a/public/filemure.css b/public/filemure.css
index a58731f..afb9991 100644
--- a/public/filemure.css
+++ b/public/filemure.css
@@ -17,3 +17,18 @@
display: block;
}
+#preview_frame {
+ position: fixed;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+ bottom: 0px;
+}
+
+#preview_container {
+ position: absolute;
+ left: 32px;
+ right: 32px;
+ top: 32px;
+ bottom: 64px;
+}
\ No newline at end of file