diff --git a/app/src/main/kotlin/controllers/SettingsController.kt b/app/src/main/kotlin/controllers/SettingsController.kt
index 971c354..51d5d6a 100644
--- a/app/src/main/kotlin/controllers/SettingsController.kt
+++ b/app/src/main/kotlin/controllers/SettingsController.kt
@@ -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)
diff --git a/domain/pom.xml b/domain/pom.xml
index 0f8f353..43382b8 100644
--- a/domain/pom.xml
+++ b/domain/pom.xml
@@ -61,6 +61,11 @@
org.jetbrains.kotlinx
kotlinx-serialization-runtime
+
+ org.apache.commons
+ commons-compress
+ 1.20
+
diff --git a/domain/src/main/kotlin/usecases/export/ExportUseCase.kt b/domain/src/main/kotlin/usecases/export/ExportUseCase.kt
index 7b71d58..2655141 100644
--- a/domain/src/main/kotlin/usecases/export/ExportUseCase.kt
+++ b/domain/src/main/kotlin/usecases/export/ExportUseCase.kt
@@ -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
}
diff --git a/domain/src/main/kotlin/usecases/export/ExportUseCaseImpl.kt b/domain/src/main/kotlin/usecases/export/ExportUseCaseImpl.kt
index 6071622..0e15e1a 100644
--- a/domain/src/main/kotlin/usecases/export/ExportUseCaseImpl.kt
+++ b/domain/src/main/kotlin/usecases/export/ExportUseCaseImpl.kt
@@ -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 {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)