備忘録:一時ファイルを使用せずメモリ上で複数ファイルをzipに固めてHttpレスポンスで返す(Kotlin, Spring Boot)

何がしたかったか

ういのセトリの管理機能でDB出力をzipに固めてダウンロードする機能があるのですが、Heroku上だとファイル保存ができないので一時ファイルを作成する方法だとできなかった。
DB出力をzipに固めてダウンロードする機能は使いたいのでメモリ上でなんとか処理できるようにする。

やったこと

zipで固める

data class File(val fileName: String, val data: ByteArray)

fun zip(files: List<File>): Optional<ByteArray> {
    val out = ByteArrayOutputStream()
    ZipOutputStream(out).apply {
        files.forEach {
            this.putNextEntry(ZipEntry(it.fileName))
            try {
                this.write(it.data)
            } catch (e: Exception) {
                e.printStackTrace()
                return Optional.empty()
            }
            this.closeEntry()
        }
        this.close()
    }
    return Optional.of(out.toByteArray())
}

ファイル名(String)とデータ本体(ByteArray)のリストを受け取って、zipに固める。
固めたzipはByteArrayにして返す。

レスポンス部分

Controllerでの処理。

@GetMapping("/databaseDump")
@ResponseBody
fun dbDumpPage(model: Model, builder: UriComponentsBuilder): ResponseEntity<ByteArray> {
    try {
        val files = mutableListOf<Commons.Companion.File>()
        // 動画一覧csv生成
        movieService.findAll().apply {
            val stringBuilder = StringBuilder()
            stringBuilder.append("movieid,name,date\n")
            this.forEach {
                stringBuilder.append("\"${it.movieId}\",\"${it.name}\",\"${it.date}\"\n")
            }
            files.add(Commons.Companion.File("movie.csv", stringBuilder.toString().toByteArray(Charsets.UTF_8)))
        }
        // その他csv作成とfilesへの追加続く

        Commons.zip(files).get().let {
            val headers = HttpHeaders().apply {
                this.add("Content-Type", "application/zip")
                this.add("Content-Disposition", "attachment; filename=dbDump.zip")
            }
            return ResponseEntity(it, headers, HttpStatus.OK)
        }
    } catch (e: Exception) {
        // よしなにエラー処理をする
    }
}

おわり

もっと冴えたやり方はあると思います。

ソースコード全体はリポジトリごと公開しています。
github.com