From 49abfa36632387c8165f0a487c6fe1a8227d4c14 Mon Sep 17 00:00:00 2001 From: Stefan Schallerl Date: Sun, 2 Feb 2025 12:30:04 +0100 Subject: [PATCH] * Adds uploading of files to the limbo. * Display limbo contents. --- README.md | 14 ++++ app/build.gradle.kts | 3 + .../main/kotlin/net/h34t/filemure/Entities.kt | 12 ++++ .../kotlin/net/h34t/filemure/FilemureApp.kt | 16 +++-- .../main/kotlin/net/h34t/filemure/Server.kt | 18 +++++- .../main/kotlin/net/h34t/filemure/Session.kt | 2 +- .../net/h34t/filemure/TemplateModifiers.kt | 3 +- app/src/main/kotlin/net/h34t/filemure/Util.kt | 12 +++- .../filemure/controller/LimboController.kt | 47 ++++++++++++++ .../filemure/controller/LoginController.kt | 15 ++--- .../filemure/controller/OverviewController.kt | 15 ++++- .../filemure/controller/UploadController.kt | 37 +++++++++++ .../filemure/repository/SqliteRepository.kt | 64 +++++++++++++++++++ app/src/main/resources/create_db.sql | 34 +++++++--- .../tpl/net.h34t.filemure.tpl/Frame.tpl.html | 6 ++ .../tpl/net.h34t.filemure.tpl/Limbo.tpl.html | 24 +++++++ .../net.h34t.filemure.tpl/Overview.tpl.html | 3 +- public/filemure.css | 19 ++++++ public/filemure.js | 47 ++++++++++++++ 19 files changed, 362 insertions(+), 29 deletions(-) create mode 100644 app/src/main/kotlin/net/h34t/filemure/Entities.kt create mode 100644 app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt create mode 100644 app/src/main/kotlin/net/h34t/filemure/controller/UploadController.kt create mode 100644 app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt create mode 100644 app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html create mode 100644 public/filemure.css create mode 100644 public/filemure.js diff --git a/README.md b/README.md index e595300..01a4eba 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,25 @@ Filemure is a simple document management application. It supports: + * uploading of files * tagging * search * download / export +## TODO + +* create document from files in limbo +* display documents list +* display document details +* display files + * in limbo + * in document +* add files to documents +* edit document data +* parse document contents (for pdf, txt, ...) +* guess target date from filenames + ## Build This project uses [Gradle](https://gradle.org/). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 558dff3..f880791 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,9 @@ plugins { } dependencies { + 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.+") implementation(libs.slf4jsimple) implementation(libs.javalin) implementation(libs.commonsText) diff --git a/app/src/main/kotlin/net/h34t/filemure/Entities.kt b/app/src/main/kotlin/net/h34t/filemure/Entities.kt new file mode 100644 index 0000000..939b66c --- /dev/null +++ b/app/src/main/kotlin/net/h34t/filemure/Entities.kt @@ -0,0 +1,12 @@ +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 45bef5f..cc1fb19 100644 --- a/app/src/main/kotlin/net/h34t/filemure/FilemureApp.kt +++ b/app/src/main/kotlin/net/h34t/filemure/FilemureApp.kt @@ -1,20 +1,25 @@ package net.h34t.filemure import io.javalin.Javalin +import net.h34t.filemure.controller.LimboController import net.h34t.filemure.controller.LoginController import net.h34t.filemure.controller.OverviewController +import net.h34t.filemure.controller.UploadController +import net.h34t.filemure.repository.SqliteRepository -class FilemureApp { +class FilemureApp(repository: SqliteRepository) { private val modifiers = TemplateModifiers() - private val loginPageController = LoginController(modifiers) + private val loginPageController = LoginController(modifiers, repository) - private val overviewController = OverviewController(modifiers) + private val overviewController = OverviewController(modifiers, repository) + private val limboController = LimboController(modifiers, repository) + private val uploadController = UploadController(modifiers, repository) fun register(server: Javalin) { server.get("/") { ctx -> - if (ctx.sessionAttribute("user") != null) { + if (ctx.getSession() != null) { overviewController.overview(ctx) } else { ctx.redirectPRG("/login") @@ -23,5 +28,8 @@ class FilemureApp { server.get("/login", loginPageController::formLogin) server.post("/login", loginPageController::doLogin) server.post("/logout", loginPageController::doLogout) + + server.post("/upload", uploadController::upload) + server.get("/limbo", limboController::formLimbo) } } \ No newline at end of file diff --git a/app/src/main/kotlin/net/h34t/filemure/Server.kt b/app/src/main/kotlin/net/h34t/filemure/Server.kt index 3802f36..b396cd4 100644 --- a/app/src/main/kotlin/net/h34t/filemure/Server.kt +++ b/app/src/main/kotlin/net/h34t/filemure/Server.kt @@ -1,13 +1,27 @@ package net.h34t.app.net.h34t.filemure import io.javalin.Javalin +import io.javalin.http.staticfiles.Location import net.h34t.filemure.FilemureApp +import net.h34t.filemure.repository.SqliteRepository + fun main() { - val app = FilemureApp() + val app = FilemureApp(SqliteRepository("jdbc:sqlite:filemure.db")) - Javalin.create(/*config*/) + Javalin + .create { config -> + config.staticFiles.add { staticFiles -> + staticFiles.hostedPath = "/" + staticFiles.directory = "./public" + staticFiles.location = Location.EXTERNAL + } + + config.requestLogger.http { ctx, ms -> + // log things here + } + } .also { app.register(it) } diff --git a/app/src/main/kotlin/net/h34t/filemure/Session.kt b/app/src/main/kotlin/net/h34t/filemure/Session.kt index 118819e..c0ce760 100644 --- a/app/src/main/kotlin/net/h34t/filemure/Session.kt +++ b/app/src/main/kotlin/net/h34t/filemure/Session.kt @@ -1,3 +1,3 @@ package net.h34t.filemure -data class Session(val email: String) \ No newline at end of file +data class Session(val id: Long, val email: String) \ No newline at end of file diff --git a/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt b/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt index ecf974e..1ed8224 100644 --- a/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt +++ b/app/src/main/kotlin/net/h34t/filemure/TemplateModifiers.kt @@ -1,10 +1,11 @@ package net.h34t.filemure import net.h34t.filemure.tpl.Frame +import net.h34t.filemure.tpl.Limbo import org.apache.commons.text.StringEscapeUtils import java.net.URLEncoder -class TemplateModifiers : Frame.Modifiers { +class TemplateModifiers : Frame.Modifiers, Limbo.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 42b31a8..577d2a3 100644 --- a/app/src/main/kotlin/net/h34t/filemure/Util.kt +++ b/app/src/main/kotlin/net/h34t/filemure/Util.kt @@ -28,4 +28,14 @@ fun Context.tempolin(tpl: Template, contentType: String = ContentType.HTML) { * @param location The location to redirect to. * */ -fun Context.redirectPRG(location: String) = this.redirect(location, HttpStatus.SEE_OTHER) \ No newline at end of file +fun Context.redirectPRG(location: String) = this.redirect(location, HttpStatus.SEE_OTHER) + +fun Context.getSession() = this.sessionAttribute("session") +fun Context.setSession(session: Session?) = this.sessionAttribute("session", session) + +private val chars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + + +fun generateExtId(): String { + return (0..8).map { chars.random() }.joinToString("") +} \ No newline at end of file diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt new file mode 100644 index 0000000..d3da439 --- /dev/null +++ b/app/src/main/kotlin/net/h34t/filemure/controller/LimboController.kt @@ -0,0 +1,47 @@ +package net.h34t.filemure.controller + +import io.javalin.http.Context +import net.h34t.filemure.TemplateModifiers +import net.h34t.filemure.getSession +import net.h34t.filemure.repository.SqliteRepository +import net.h34t.filemure.tempolin +import net.h34t.filemure.tpl.Frame +import net.h34t.filemure.tpl.Limbo +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +class LimboController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { + + private val dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT) + + fun formLimbo(ctx: Context) { + val session = ctx.getSession() + requireNotNull(session) + + + val files = repository.getFilesInLimbo(accountId = session.id) + + ctx.tempolin( + Frame( + modifiers = modifiers, + title = "Filemure Limbo", + isTarget = true, + content = Limbo( + modifiers = modifiers, + limboFileCount = files.size.toString(), + file = { + files.map { f -> + FileBlock( + id = f.id.toString(), + file = f.filename, + type = f.contentType, + size = f.size.toString(), + uploaded = f.created.format(dtf) + ) + }.asSequence() + } + ) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/LoginController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/LoginController.kt index 353f813..6f543b7 100644 --- a/app/src/main/kotlin/net/h34t/filemure/controller/LoginController.kt +++ b/app/src/main/kotlin/net/h34t/filemure/controller/LoginController.kt @@ -1,20 +1,19 @@ package net.h34t.filemure.controller import io.javalin.http.Context -import net.h34t.filemure.Session -import net.h34t.filemure.TemplateModifiers -import net.h34t.filemure.redirectPRG -import net.h34t.filemure.tempolin +import net.h34t.filemure.* +import net.h34t.filemure.repository.SqliteRepository import net.h34t.filemure.tpl.Frame import net.h34t.filemure.tpl.Login -class LoginController(val modifiers: TemplateModifiers) { +class LoginController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { fun formLogin(ctx: Context) { ctx.tempolin( Frame( modifiers = modifiers, title = "Hello to Filemure", + isTarget = false, content = Login( ) @@ -27,16 +26,16 @@ class LoginController(val modifiers: TemplateModifiers) { val password = ctx.formParam("password") if (username == "stefan@schallerl.com" && password == "foobar") { - ctx.sessionAttribute("user", Session(email = username)) + ctx.setSession(Session(id = 1, email = username)) ctx.redirectPRG("/") } else { - ctx.sessionAttribute("user", null) + ctx.setSession(null) ctx.redirectPRG("/") } } fun doLogout(ctx: Context) { - ctx.sessionAttribute("user", null) + ctx.setSession(null) ctx.redirectPRG("/login") } } \ No newline at end of file diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt index 2e71d5e..8dddc2d 100644 --- a/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt +++ b/app/src/main/kotlin/net/h34t/filemure/controller/OverviewController.kt @@ -2,18 +2,29 @@ package net.h34t.filemure.controller import io.javalin.http.Context import net.h34t.filemure.TemplateModifiers +import net.h34t.filemure.getSession +import net.h34t.filemure.repository.SqliteRepository import net.h34t.filemure.tempolin import net.h34t.filemure.tpl.Frame import net.h34t.filemure.tpl.Overview -class OverviewController(val modifiers: TemplateModifiers) { +class OverviewController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { fun overview(ctx: Context) { + val session = ctx.getSession() + requireNotNull(session) + + + val limboFileCount = repository.getLimboFileCount(accountId = session.id) + ctx.tempolin( Frame( modifiers = modifiers, title = "Filemure Overview", - content = Overview() + isTarget = true, + content = Overview( + limboFileCount = limboFileCount.toString() + ) ) ) } diff --git a/app/src/main/kotlin/net/h34t/filemure/controller/UploadController.kt b/app/src/main/kotlin/net/h34t/filemure/controller/UploadController.kt new file mode 100644 index 0000000..701e077 --- /dev/null +++ b/app/src/main/kotlin/net/h34t/filemure/controller/UploadController.kt @@ -0,0 +1,37 @@ +package net.h34t.filemure.controller + +import io.javalin.http.Context +import net.h34t.filemure.TemplateModifiers +import net.h34t.filemure.getSession +import net.h34t.filemure.repository.SqliteRepository + +class UploadController(val modifiers: TemplateModifiers, val repository: SqliteRepository) { + + fun upload(ctx: Context) { + println("upload") + val session = ctx.getSession() + requireNotNull(session) + + val accountid = session.id + + val files = ctx.uploadedFiles() + + files.forEach { + println("filename: " + it.filename()) + println("ext: " + it.extension()) + println("contentType: " + it.contentType()) // mime + println("size: " + it.size()) + + it.contentAndClose { content -> + repository.addFileToLimbo(accountid, it.filename(), it.contentType(), it.size(), content) + } + } + + ctx.status(200) + ctx.json(Result(files = files.size)) + } + + data class Result( + val files: Int + ) +} diff --git a/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt b/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt new file mode 100644 index 0000000..774327e --- /dev/null +++ b/app/src/main/kotlin/net/h34t/filemure/repository/SqliteRepository.kt @@ -0,0 +1,64 @@ +package net.h34t.filemure.repository + +import net.h34t.filemure.LimboFile +import net.h34t.filemure.generateExtId +import java.io.InputStream +import java.sql.Connection +import java.sql.DriverManager +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class SqliteRepository(url: String) { + + private val dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + + val connection: Connection = DriverManager.getConnection(url) + + fun addFileToLimbo(accountId: Long, filename: String, contentType: String?, size: Long, content: InputStream) { + connection.prepareStatement("INSERT INTO limbo (account_id, ext_id, filename, content_type, file_size, created, content) VALUES (?,?,?,?,?,datetime(),?)") + .use { stmt -> + stmt.setLong(1, accountId) + stmt.setString(2, generateExtId()) + stmt.setString(3, filename) + stmt.setString(4, contentType) + stmt.setLong(5, size) + stmt.setBytes(6, content.readAllBytes()) + + val res = stmt.executeUpdate() + + require(res == 1) + } + } + + fun getLimboFileCount(accountId: Long): Long { + connection.prepareStatement("SELECT count(*) as count FROM limbo WHERE account_id=?").use { stmt -> + stmt.setLong(1, accountId) + 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 limbo WHERE account_id=? ORDER BY created DESC") + .use { stmt -> + stmt.setLong(1, accountId) + val res = stmt.executeQuery() + + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/resources/create_db.sql b/app/src/main/resources/create_db.sql index c32e439..264f129 100644 --- a/app/src/main/resources/create_db.sql +++ b/app/src/main/resources/create_db.sql @@ -8,20 +8,35 @@ CREATE TABLE account ( unique(email) ); - -- document definition CREATE TABLE document ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, ext_id TEXT NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, tags TEXT NOT NULL, created TEXT NOT NULL, - reference_date TEXT + reference_date TEXT, + CONSTRAINT document_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE, + unique(ext_id) ); -CREATE UNIQUE INDEX document_ext_id_idx ON document (ext_id); +-- limbo definition + +CREATE TABLE limbo ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL, + ext_id TEXT NOT NULL, + filename TEXT NOT NULL, + content_type TEXT, + file_size INTEGER NOT NULL, + content BLOB NOT NULL, + created TEXT NOT NULL, + unique(ext_id), + CONSTRAINT limbo_account_FK FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE, +); -- file definition @@ -29,12 +44,13 @@ CREATE UNIQUE INDEX document_ext_id_idx ON document (ext_id); CREATE TABLE file ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, document_id INTEGER NOT NULL, - ext_id TEXT NOT NULL, - name TEXT NOT NULL, - created INTEGER NOT NULL, - contents BLOB NOT NULL, - CONSTRAINT file_document_FK FOREIGN KEY (document_id) REFERENCES document(id) ON DELETE CASCADE + filename TEXT NOT NULL, + content BLOB NOT NULL, + content_type TEXT, + content_extracted TEXT, + created TEXT NOT NULL, + CONSTRAINT file_document_FK FOREIGN KEY (document_id) REFERENCES document(id) ON DELETE CASCADE, + unique(ext_id) ); -CREATE UNIQUE INDEX file_ext_id_idx ON file (ext_id); \ No newline at end of file diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/Frame.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/Frame.tpl.html index d995fc0..54bfe1a 100644 --- a/app/src/main/tpl/net.h34t.filemure.tpl/Frame.tpl.html +++ b/app/src/main/tpl/net.h34t.filemure.tpl/Frame.tpl.html @@ -2,9 +2,15 @@ + {*$title} + + +{if $isTarget} +
+{/if} {template $content} \ 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 new file mode 100644 index 0000000..72cbdac --- /dev/null +++ b/app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html @@ -0,0 +1,24 @@ +

Filemure Limbo

+ +

{$limboFileCount} Files

+
+ + + + + + + + + {for $file} + + + + + + + + {/for} +
-FilenameTypeSizeUploaded
{*$file}{*$type}{*$size}{*$uploaded}
+ +
\ No newline at end of file diff --git a/app/src/main/tpl/net.h34t.filemure.tpl/Overview.tpl.html b/app/src/main/tpl/net.h34t.filemure.tpl/Overview.tpl.html index 35e3282..2727cc0 100644 --- a/app/src/main/tpl/net.h34t.filemure.tpl/Overview.tpl.html +++ b/app/src/main/tpl/net.h34t.filemure.tpl/Overview.tpl.html @@ -1,3 +1,4 @@

Hello to Filemure

-Your files: \ No newline at end of file +Files in limbo: {$limboFileCount}. + diff --git a/public/filemure.css b/public/filemure.css new file mode 100644 index 0000000..a58731f --- /dev/null +++ b/public/filemure.css @@ -0,0 +1,19 @@ +.dropzone { + position: fixed; + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; + /* visibility: visible; */ + display: none; + border: 0px dotted red; + margin: 4px; +} + +.dropzone.dragactive { + background-color: #33FF0000; + border: 3px dotted blue; + /* visibility: visible; */ + display: block; +} + diff --git a/public/filemure.js b/public/filemure.js new file mode 100644 index 0000000..0843a2f --- /dev/null +++ b/public/filemure.js @@ -0,0 +1,47 @@ +document.addEventListener("DOMContentLoaded", function() { + console.log("filemure ready") + const dropzone = document.querySelector('html'); + + // window.addEventListener + + // Prevent default behavior for drag-over and drop + dropzone.addEventListener('dragover', (e) => { + console.log("over") + e.preventDefault(); + dropzone.classList.add('dragactive'); + }); + + dropzone.addEventListener('dragleave', (e) => { + console.log("out") + dropzone.classList.remove('dragactive'); + }); + + dropzone.addEventListener('drop', (e) => { + console.log("drop") + e.preventDefault(); + dropzone.classList.remove('dragactive'); + + // Handle dropped files + const files = e.dataTransfer.files; + if (files.length > 0) { + console.log('Files dropped:', files); + + // Process the files + for (const file of files) { + console.log('File name:', file.name); + console.log('File size:', file.size, 'bytes'); + console.log('File type:', file.type); + + const formData = new FormData(); + formData.append('file', file); + + fetch('/upload', { + method: 'POST', + body: formData, + }).then(response => response.json()) + .then(data => console.log('Upload successful:', data)) + .catch(error => console.error('Upload failed:', error)); + } + } + }); +}); \ No newline at end of file