* Adds uploading of files to the limbo.
* Display limbo contents.
This commit is contained in:
parent
7696868f9b
commit
49abfa3663
19 changed files with 362 additions and 29 deletions
14
README.md
14
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/).
|
||||
|
|
|
@ -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)
|
||||
|
|
12
app/src/main/kotlin/net/h34t/filemure/Entities.kt
Normal file
12
app/src/main/kotlin/net/h34t/filemure/Entities.kt
Normal file
|
@ -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,
|
||||
)
|
|
@ -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<Session>("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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
package net.h34t.filemure
|
||||
|
||||
data class Session(val email: String)
|
||||
data class Session(val id: Long, val email: String)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
fun Context.redirectPRG(location: String) = this.redirect(location, HttpStatus.SEE_OTHER)
|
||||
|
||||
fun Context.getSession() = this.sessionAttribute<Session>("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("")
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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<LimboFile> {
|
||||
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<LimboFile>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -2,9 +2,15 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{*$title}</title>
|
||||
<link rel="stylesheet" type="text/css" href="./filemure.css">
|
||||
<script src="/filemure.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
{if $isTarget}
|
||||
<div class="dropzone"></div>
|
||||
{/if}
|
||||
{template $content}
|
||||
</body>
|
||||
</html>
|
24
app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html
Normal file
24
app/src/main/tpl/net.h34t.filemure.tpl/Limbo.tpl.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<h1>Filemure Limbo</h1>
|
||||
|
||||
<p>{$limboFileCount} Files</p>
|
||||
<form action="/document" method="post">
|
||||
<table>
|
||||
<tr>
|
||||
<th>-</th>
|
||||
<th>Filename</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th>Uploaded</th>
|
||||
</tr>
|
||||
{for $file}
|
||||
<tr>
|
||||
<td><label><input type="checkbox" name="limbofile" value="{*$id}"></label></td>
|
||||
<td>{*$file}</td>
|
||||
<td>{*$type}</td>
|
||||
<td>{*$size}</td>
|
||||
<td>{*$uploaded}</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</table>
|
||||
<input type="submit" value="new document">
|
||||
</form>
|
|
@ -1,3 +1,4 @@
|
|||
<h1>Hello to Filemure</h1>
|
||||
|
||||
Your files:
|
||||
Files in <a href="/limbo">limbo: {$limboFileCount}</a>.
|
||||
|
||||
|
|
19
public/filemure.css
Normal file
19
public/filemure.css
Normal file
|
@ -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;
|
||||
}
|
||||
|
47
public/filemure.js
Normal file
47
public/filemure.js
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue