【kotlin】日本語ファイルを ZIP 圧縮する


はじめに

ZIP 圧縮時、つまりZipOutputStream で使われる Charset のデフォルト値は UTF-8 となっています。

Windows の日本語ファイル名は MS932 なので、このままでは Windows で解凍した際に文字化けします。
そこで、ZipOutputStream で Charset を指定し、文字化け回避します。
似たような記事は Java ならたくさんありますが、Kotlin では少なかったので投下します。

尚、筆者は FileInputStream の使い方が分からないくらい Kotlin(Java) に慣れていないため、もっと良い書き方があればご教示いただけると嬉しいです。

この記事で分かること

Kotlin を使用した ZIP 圧縮(日本語対応)のやり方。

※展開はやりません。

参考にさせていただいた記事様

コード

import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.charset.Charset
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

/**
 * 指定された ArrayList のファイルを ZIP アーカイブし、指定されたパスに作成します。
 * デフォルト文字コードは Shift_JIS ですので、日本語ファイル名も対応できます。
 *
 * @param fromFiles 圧縮するファイルリスト。  ( 例; {C:/sample1.txt, C:/sample2.txt} )
 * @param toZipFile 圧縮後のファイル名をフルパスで指定。 ( 例: C:/sample.zip )
 * @param charset  Zip化する際に使用するCharset。 ※1
 */
fun zipFiles(fromFiles: List<File>, toZipFile: File, charset: Charset = Charset.forName("MS932")) {
    try {
        // use を使うと、スコープが終わるタイミングで自動的にResourceをCloseしてくれます。
        // ZipOutputStreamの第1引数には、zip化するファイルを指定します。
        // ZipOutputStreamの第2引数が空の場合、CharsetがUTF-8となります。
        ZipOutputStream(BufferedOutputStream(FileOutputStream(toZipFile)), charset).use { zipOutputStream ->
            fromFiles.forEach { file ->
                if (file.isFile) {
                    archiveFile(zipOutputStream, file, file.name)
                } else if (file.isDirectory) {
                    archiveDirectory(zipOutputStream, file, file)
                }
            }
        }
    } catch (e: Exception) {
        // エラー処理。
    }
}

/**
 * 指定された ArrayList のファイルを ZIP アーカイブし、指定されたパスに作成します。
 * デフォルト文字コードは Shift_JIS ですので、日本語ファイル名も対応できます。
 *
 * @param zipOutputStream targetFileを書き込むStream。
 * @param baseFile 圧縮するディレクトリのルートファイル。
 * @param targetFile 圧縮するディレクトリ。
 */
fun archiveDirectory(zipOutputStream: ZipOutputStream, baseFile: File, targetFile: File) {
    targetFile.listFiles()?.forEach { file ->
        if (file.isDirectory) {
            archiveDirectory(zipOutputStream, baseFile, file)
        } else if (file.isFile) {
            // entryNameは相対パスで指定する必要があるため、baseFileとの相対パスを取得します。
            val relativeEntryName = file.toRelativeString(baseFile.parentFile)
            archiveFile(zipOutputStream, file, relativeEntryName)
        } else {
            // ファイルが存在しない場合、isDirectoryにもisFileにも該当しません。
            // その場合に処理を行いたい場合、ここに処理を書きます。
        }
    }
}

/**
 * 指定された ArrayList のファイルを ZIP アーカイブし、指定されたパスに作成します。
 * デフォルト文字コードは Shift_JIS ですので、日本語ファイル名も対応できます。
 *
 * @param zipOutputStream targetFileを書き込むStream。
 * @param targetFile 圧縮するファイル。
 * @param entryName targetFileをzipOutputStreamに書き込む際のファイル名(相対パス)。
 *          ex) hoge/fuga.txt であれば、 fizz.zip/hoge/fuga.txt にファイルが作られます。
 */
fun archiveFile(zipOutputStream: ZipOutputStream, targetFile: File, entryName: String) {
    // ※2
    val zipBufferSize = 100 * 1024
    // ${ZipOutputStreamで指定したFileのPath} / ${entryName} にファイルが作られます。
    zipOutputStream.putNextEntry(ZipEntry(entryName))
    FileInputStream(targetFile).use { inputStream ->
        val bytes = ByteArray(zipBufferSize)
        var length: Int
        // zipBufferSize分ファイルを読み込む。読み込み終わると -1 を返します。
        while (inputStream.read(bytes).also { length = it } != -1) {
            zipOutputStream.write(bytes, 0, length)
        }
    }
    // Entryは use で Closeされないため、手動でCloseする必要があります。
    zipOutputStream.closeEntry()
}

Note

※1 使用する Charset について

今回、使用する Charset として MS932 を選択しています。
同じような文字コードである SHIFT_JIS ではなく MS932 を使用する理由は、以下の記事を参考にさせていただきました。

※2 FileInputStream で使用するバッファサイズについて

参考: Java ファイルコピー(バッファサイズを変更)

今回はアップロードされるファイルのサイズの想定が 1MB ~5MB 程度だったので、100KB を指定しました。

net.lingala.zip4j.ZipFile について

便利な機能の多いnet.lingala.zip4j.ZipFile はCharsetの変更ができません。
そのため、日本語対応したい場合はjava.util.zipを使いましょう。