Skip to content

offlineImport.kt

src/jvmMain/kotlin/afield/app/server/offlineImport.kt · 43 lines

package afield.app.server

import afield.document.persistence.SavingContext
import java.io.ByteArrayInputStream
import java.io.File
import java.util.zip.ZipInputStream

fun unpackDocumentZip(zip: ByteArray, targetDir: File): Int

Unpacks a document zip, as the docs pages ship them, into targetDir, for /api/import-offline-doc: metadata.edn together with its sidecars and every folder beside it (saved chats, study revisions, recorded conversations), which an ednText import leaves behind.

A zip whose entries all sit in one top-level folder (function_conversation_example/..., the layout a download unzips to) has that folder removed, so the document lands in targetDir itself. An entry that would resolve outside targetDir fails the import. Returns the number of files written; fails when the zip holds no metadata.edn.

fun unpackDocumentZip(zip: ByteArray, targetDir: File): Int {
    val entries = mutableListOf<Pair<String, ByteArray>>()
    ZipInputStream(ByteArrayInputStream(zip)).use { input ->
        while (true) {
            val entry = input.nextEntry ?: break
            if (!entry.isDirectory) entries += entry.name.replace('\\', '/') to input.readBytes()
        }
    }
    val roots = entries.map { it.first.substringBefore('/', "") }.toSet()
    val prefix = roots.singleOrNull()?.takeIf { it.isNotEmpty() }?.let { "$it/" } ?: ""
    val files = entries.map { (name, data) -> name.removePrefix(prefix) to data }
    require(files.any { it.first == SavingContext.METADATA_EDN }) { "the zip has no ${SavingContext.METADATA_EDN}" }

    val root = targetDir.canonicalFile
    for ((name, _) in files) {
        val target = File(root, name).canonicalFile
        require(target.toPath().startsWith(root.toPath()) && target != root) { "zip entry outside the document: $name" }
    }
    for ((name, data) in files) {
        val target = File(root, name)
        target.parentFile.mkdirs()
        target.writeBytes(data)
    }
    return files.size
}