Import feature
This commit is contained in:
parent
724fa0483e
commit
195c7c10ac
@ -14,6 +14,7 @@ dependencies {
|
||||
implementation(project(":css"))
|
||||
|
||||
implementation(libs.http4k.core)
|
||||
implementation(libs.http4k.multipart)
|
||||
implementation(libs.bundles.jetty)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
|
||||
@ -2,21 +2,19 @@ package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.DeleteError
|
||||
import be.simplenotes.domain.DeleteForm
|
||||
import be.simplenotes.domain.ExportService
|
||||
import be.simplenotes.domain.UserService
|
||||
import be.simplenotes.domain.*
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.views.SettingView
|
||||
import jakarta.inject.Singleton
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.body.form
|
||||
import org.http4k.core.cookie.invalidateCookie
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SettingsController(
|
||||
private val userService: UserService,
|
||||
private val exportService: ExportService,
|
||||
private val importService: ImportService,
|
||||
private val settingView: SettingView,
|
||||
) {
|
||||
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
@ -36,6 +34,7 @@ class SettingsController(
|
||||
error = "Wrong password",
|
||||
),
|
||||
)
|
||||
|
||||
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
||||
settingView.settings(
|
||||
loggedInUser,
|
||||
@ -79,4 +78,12 @@ class SettingsController(
|
||||
|
||||
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
|
||||
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
|
||||
|
||||
fun import(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val form = MultipartFormBody.from(request)
|
||||
val file = form.file("file") ?: return Response(Status.BAD_REQUEST)
|
||||
val json = file.content.bufferedReader().readText()
|
||||
importService.importJson(loggedInUser, json)
|
||||
return Response.redirect("/notes")
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,7 @@ class SettingsRoutes(
|
||||
"/settings" bind GET to settingsController::settings,
|
||||
"/settings" bind POST to settingsController::settings,
|
||||
"/export" bind POST to settingsController::export,
|
||||
"/import" bind POST to settingsController::import,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -17,6 +17,6 @@ internal class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
TODO("Not implemented, isn't needed")
|
||||
return LocalDateTime.parse(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#note {
|
||||
a {
|
||||
@apply text-blue-700 underline;
|
||||
@apply text-blue-500 underline;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
30
domain/src/ImportService.kt
Normal file
30
domain/src/ImportService.kt
Normal file
@ -0,0 +1,30 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import be.simplenotes.domain.security.HtmlSanitizer
|
||||
import be.simplenotes.persistence.repositories.NoteRepository
|
||||
import be.simplenotes.types.ExportedNote
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import jakarta.inject.Singleton
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
interface ImportService {
|
||||
fun importJson(user: LoggedInUser, content: String)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class ImportServiceImpl(
|
||||
private val noteRepository: NoteRepository,
|
||||
private val json: Json,
|
||||
private val htmlSanitizer: HtmlSanitizer,
|
||||
) : ImportService {
|
||||
override fun importJson(user: LoggedInUser, content: String) {
|
||||
val notes = json.decodeFromString(ListSerializer(ExportedNote.serializer()), content)
|
||||
noteRepository.import(
|
||||
user.userId,
|
||||
notes.map {
|
||||
it.copy(html = htmlSanitizer.sanitize(user, it.html))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,7 @@ internal class MarkdownServiceImpl(
|
||||
private fun splitMetaFromDocument(input: String) = either {
|
||||
val split = input.split(yamlBoundPattern, 3)
|
||||
ensure(split.size >= 3) { MarkdownParsingError.MissingMeta }
|
||||
(split[1].trim() to split[2].trim())
|
||||
split[1].trim() to split[2].trim()
|
||||
}
|
||||
|
||||
private val yaml = Yaml()
|
||||
|
||||
@ -20,6 +20,7 @@ lucene-analyzers-common = { module = "org.apache.lucene:lucene-analysis-common",
|
||||
lucene-queryparser = { module = "org.apache.lucene:lucene-queryparser", version.ref = "lucene" }
|
||||
|
||||
http4k-core = { module = "org.http4k:http4k-core", version.ref = "http4k" }
|
||||
http4k-multipart = { module = "org.http4k:http4k-multipart", version.ref = "http4k" }
|
||||
http4k-testing-hamkrest = { module = "org.http4k:http4k-testing-hamkrest", version.ref = "http4k" }
|
||||
|
||||
jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" }
|
||||
|
||||
@ -26,6 +26,8 @@ interface NoteRepository {
|
||||
fun create(userId: Int, note: Note): PersistedNote
|
||||
fun find(userId: Int, uuid: UUID): PersistedNote?
|
||||
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
||||
fun import(userId: Int, notes: List<ExportedNote>)
|
||||
|
||||
fun export(userId: Int): List<ExportedNote>
|
||||
fun findAllDetails(userId: Int): List<PersistedNote>
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package be.simplenotes.persistence.repositories
|
||||
import be.simplenotes.persistence.*
|
||||
import be.simplenotes.persistence.converters.NoteConverter
|
||||
import be.simplenotes.persistence.extensions.arrayContains
|
||||
import be.simplenotes.types.ExportedNote
|
||||
import be.simplenotes.types.Note
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import org.ktorm.database.Database
|
||||
@ -118,6 +119,43 @@ internal class NoteRepositoryImpl(
|
||||
.map { it.getInt(1) }
|
||||
.first()
|
||||
|
||||
override fun import(userId: Int, notes: List<ExportedNote>) {
|
||||
if (notes.isEmpty()) return
|
||||
val notesByID = notes.associateBy { UUID.randomUUID() }
|
||||
val tags = sequence<Pair<UUID, String>> {
|
||||
notesByID.entries.forEach { (key, value) ->
|
||||
value.tags.forEach { tag ->
|
||||
yield(key to tag)
|
||||
}
|
||||
}
|
||||
}.toList()
|
||||
|
||||
db.batchInsert(Notes) {
|
||||
notesByID.forEach { (uuid, note) ->
|
||||
item {
|
||||
set(it.uuid, uuid)
|
||||
set(it.userId, userId)
|
||||
set(it.title, note.title)
|
||||
set(it.html, note.html)
|
||||
set(it.markdown, note.markdown)
|
||||
set(it.updatedAt, note.updatedAt)
|
||||
set(it.deleted, note.trash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags.takeIf { it.isNotEmpty() }?.run {
|
||||
db.batchInsert(Tags) {
|
||||
forEach { (uuid, tagName) ->
|
||||
item {
|
||||
set(it.noteUuid, uuid)
|
||||
set(it.name, tagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun export(userId: Int) = db.noteWithTags
|
||||
.filterColumns { it.columns - it.userId - it.public }
|
||||
.filter { it.userId eq userId }
|
||||
|
||||
@ -6,10 +6,13 @@ import be.simplenotes.views.components.alert
|
||||
import be.simplenotes.views.components.input
|
||||
import be.simplenotes.views.extensions.summary
|
||||
import io.konform.validation.ValidationError
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ButtonType.submit
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ButtonType.submit
|
||||
import kotlinx.html.FormEncType.multipartFormData
|
||||
import kotlinx.html.FormMethod.post
|
||||
import kotlinx.html.InputType.file
|
||||
|
||||
@Singleton
|
||||
class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
@ -28,22 +31,26 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
}
|
||||
|
||||
section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") {
|
||||
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||
form(classes = "m-2 flex-1", method = post, action = "/export") {
|
||||
button(
|
||||
name = "display",
|
||||
classes = "inline btn btn-teal block",
|
||||
classes = "btn btn-teal block",
|
||||
type = submit,
|
||||
) { +"Display my data" }
|
||||
}
|
||||
|
||||
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||
form(classes = "m-2 flex-1", method = post, action = "/export") {
|
||||
div {
|
||||
listOf("json", "zip").forEach { format ->
|
||||
radioInput(name = "format") {
|
||||
div("px-2") {
|
||||
radioInput(
|
||||
name = "format",
|
||||
classes =
|
||||
"checked:bg-blue-500 bg-gray-200 appearance-none rounded-full border-2 h-5 w-5",
|
||||
) {
|
||||
id = format
|
||||
attributes["value"] = format
|
||||
if (format == "json") attributes["checked"] = ""
|
||||
else attributes["class"] = "ml-4"
|
||||
}
|
||||
label(classes = "ml-2") {
|
||||
attributes["for"] = format
|
||||
@ -51,11 +58,28 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button(name = "download", classes = "inline btn btn-green block mt-2", type = submit) {
|
||||
button(name = "download", classes = "btn btn-green block mt-2", type = submit) {
|
||||
+"Download my data"
|
||||
}
|
||||
}
|
||||
|
||||
form(classes = "m-2 flex-1", method = post, encType = multipartFormData, action = "/import") {
|
||||
input(
|
||||
file,
|
||||
classes = "file:hidden mb-4",
|
||||
name = "file",
|
||||
) {
|
||||
attributes["accept"] = ".json,application/json"
|
||||
attributes["required"] = ""
|
||||
}
|
||||
button(
|
||||
name = "import",
|
||||
classes = "btn btn-teal block",
|
||||
type = submit,
|
||||
) { +"Import" }
|
||||
}
|
||||
}
|
||||
|
||||
section(classes = "m-4 p-4 bg-gray-800 rounded") {
|
||||
@ -77,7 +101,7 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
}
|
||||
}
|
||||
|
||||
form(classes = "mt-4", method = FormMethod.post) {
|
||||
form(classes = "mt-4", method = post) {
|
||||
input(
|
||||
id = "password",
|
||||
placeholder = "Password",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user