Compare commits

...

2 Commits

5 changed files with 92 additions and 21 deletions

View File

@ -7,10 +7,7 @@ import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.*
import org.http4k.core.body.form
import org.http4k.core.cookie.invalidateCookie
@ -49,18 +46,29 @@ class SettingsController(
)
}
fun export(request: Request, jwtPayload: JwtPayload): Response {
val isDownload = request.form("download") != null
val json = userService.export(jwtPayload.userId)
val res = Response(Status.OK).body(json).header("Content-Type", "application/json")
return if (isDownload) res.header(
"Content-Disposition",
"attachment; filename=\"simplenotes-export-${sanitizeFilename(jwtPayload.username)}.json\""
)
else res
private fun attachment(filename: String, contentType: String) = { response: Response ->
val name = filename.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
response
.header("Content-Disposition", "attachment; filename=\"$name\"")
.header("Content-Type", contentType)
}
private fun sanitizeFilename(inputName: String): String = inputName.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
fun export(request: Request, jwtPayload: JwtPayload): Response {
val isDownload = request.form("download") != null
return if (isDownload) {
val filename = "simplenotes-export-${jwtPayload.username}"
if (request.form("format") == "zip") {
val zip = userService.exportAsZip(jwtPayload.userId)
Response(Status.OK)
.with(attachment("$filename.zip", "application/zip"))
.body(zip)
} else
Response(Status.OK)
.with(attachment("$filename.json", "application/json"))
.body(userService.exportAsJson(jwtPayload.userId))
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json")
}
private fun Request.deleteForm(jwtPayload: JwtPayload) =
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)

View File

@ -26,14 +26,31 @@ class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResol
}
}
section("m-4 p-4 bg-gray-800 rounded") {
p(classes = "mb-4") {
+"Export all my data"
section("m-4 p-4 bg-gray-800 rounded flex justify-around") {
form(method = FormMethod.post, action = "/export") {
button(name = "display",
classes = "inline btn btn-teal block",
type = submit) { +"Display my data" }
}
form(method = FormMethod.post, action = "/export") {
button(name = "display", classes = "inline btn btn-teal block", type = submit) { +"Display my data" }
button(name = "download", classes = "inline btn btn-green block ml-2 mt-2", type = submit) {
listOf("json", "zip").forEach { format ->
div {
radioInput(name = "format") {
id = format
attributes["value"] = format
if(format == "json") attributes["checked"] = ""
}
label(classes = "ml-2") {
attributes["for"] = format
+format
}
}
}
button(name = "download", classes = "inline btn btn-green block mt-2", type = submit) {
+"Download my data"
}
}

View File

@ -61,6 +61,11 @@
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
</dependencies>
</project>

View File

@ -1,5 +1,8 @@
package be.simplenotes.domain.usecases.export
import java.io.InputStream
interface ExportUseCase {
fun export(userId: Int): String
fun exportAsJson(userId: Int): String
fun exportAsZip(userId: Int): InputStream
}

View File

@ -11,10 +11,15 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.time.LocalDateTime
internal class ExportUseCaseImpl(private val noteRepository: NoteRepository) : ExportUseCase {
override fun export(userId: Int): String {
override fun exportAsJson(userId: Int): String {
val module = SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer)
}
@ -27,8 +32,41 @@ internal class ExportUseCaseImpl(private val noteRepository: NoteRepository) : E
val notes = noteRepository.export(userId)
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
}
private fun sanitizeFilename(inputName: String): String = inputName.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
override fun exportAsZip(userId: Int): InputStream {
val notes = noteRepository.export(userId)
val zipOutput = ZipOutput()
zipOutput.use { zip ->
notes.forEach {
val name = sanitizeFilename(it.title)
zip.write("notes/$name.md", it.markdown)
}
}
return ByteArrayInputStream(zipOutput.outputStream.toByteArray())
}
}
class ZipOutput : AutoCloseable {
val outputStream = ByteArrayOutputStream()
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
fun write(path: String, content: String) {
val entry = ZipArchiveEntry(path)
zipOutputStream.putArchiveEntry(entry)
zipOutputStream.write(content.toByteArray())
zipOutputStream.closeArchiveEntry()
}
override fun close() {
zipOutputStream.finish()
zipOutputStream.close()
}
}
internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)