Java Tips | I/O・ネットワーク:再帰zip

Java Java
スポンサーリンク

「再帰zip」ユーティリティが業務で活きる場面

再帰zipという言葉が少し硬く聞こえるかもしれませんが、やっていることはシンプルです。 「あるディレクトリを指定したら、その配下のサブディレクトリも含めて、全部まとめて ZIP にする」——これが再帰zipです。

業務では、次のような場面でよく使われます。

  • あるプロジェクトフォルダ一式をバックアップしたい
  • ユーザーに「このフォルダ丸ごとダウンロード」させたい
  • ログディレクトリ配下を日次で ZIP に固めて保存したい

毎回「どのファイルを ZIP に入れるか」を手で列挙するのは現実的ではありません。 だからこそ、「ディレクトリを渡せば、配下を再帰的に ZIP にしてくれるユーティリティ」があると、業務コードが一気に楽になります。

再帰zipの基本構造をイメージする

「ディレクトリを歩く」+「ZipOutputStreamに書く」

再帰zipの本質は、この二つの組み合わせです。

  • ディレクトリ配下を再帰的にたどる
  • 見つかったファイルを ZipOutputStream に順番に書いていく

Java では、Files.walk を使うと「再帰的にたどる」が簡単に書けます。 それを ZipOutputStream と組み合わせる形が定番です。

再帰zipユーティリティの具体例

ベースディレクトリ配下を丸ごと ZIP にする

まずは、分かりやすい形でコードを見てみましょう。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class RecursiveZipUtils {

    public static void zipDirectory(Path baseDir, Path zipFile) throws IOException {
        try (OutputStream out = Files.newOutputStream(zipFile);
             ZipOutputStream zipOut = new ZipOutputStream(out)) {

            Files.walk(baseDir)
                 .filter(Files::isRegularFile)
                 .forEach(path -> {
                     try {
                         Path relative = baseDir.relativize(path);
                         String entryName = relative.toString().replace("\\", "/");

                         ZipEntry entry = new ZipEntry(entryName);
                         zipOut.putNextEntry(entry);

                         try (InputStream in = Files.newInputStream(path)) {
                             byte[] buffer = new byte[8192];
                             int len;
                             while ((len = in.read(buffer)) != -1) {
                                 zipOut.write(buffer, 0, len);
                             }
                         }

                         zipOut.closeEntry();
                     } catch (IOException e) {
                         throw new RuntimeException(e);
                     }
                 });
        }
    }
}
Java

使い方のイメージはこうです。

Path baseDir = Path.of("data/project");
Path zip     = Path.of("backup/project.zip");

RecursiveZipUtils.zipDirectory(baseDir, zip);
Java

このユーティリティを呼ぶだけで、data/project 配下のファイルが、 ディレクトリ構造ごと project.zip にまとめられます。

重要ポイント1:相対パスでエントリ名を作る理由

ここで一番深掘りしたいのが、「エントリ名の作り方」です。

baseDir.relativize(path) で「ベースディレクトリからの相対パス」を作り、 それを ZipEntry の名前にしています。

例えば、

  • ベースディレクトリ: data/project
  • 実ファイル: data/project/logs/app.log

なら、relativelogs/app.log になります。

この相対パスをエントリ名にすることで、ZIP を展開したときに、 logs/app.log というディレクトリ構造が再現されます。

もしここで絶対パス(C:\.../home/...)をそのまま使ってしまうと、 ZIP ビューアによってはおかしな場所に展開されたり、 他環境で意味のないパスになったりしてしまいます。

「ZIP の中には“ベースディレクトリからの相対パス”だけを入れる」 この感覚は、業務で再帰zipを扱ううえでかなり重要です。

重要ポイント2:「/」区切りに統一する理由

relative.toString() をそのまま使うと、Windows では logs\app.log のように \ 区切りになります。 しかし、ZIP のエントリ名は基本的に / 区切りで扱うのが安全です。

そこで、replace("\\", "/") を入れて、 logs/app.log のように統一しています。

これをしておくと、Windows でも Linux でも、 ZIP ビューアや解凍ツールが同じようにディレクトリ構造を解釈してくれます。

「ZIP の中のパスは OS ではなく“ZIP文化”に合わせる」 という意識を持っておくと、環境差で悩まされることが減ります。

重要ポイント3:部分読み込みと部分書き込み

再帰zipは、たくさんのファイルをまとめるので、 「全部を一気にメモリに載せる」設計は危険です。

ユーティリティでは、次のように「少しずつ読みながら書く」形にしています。

byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
    zipOut.write(buffer, 0, len);
}
Java

ここで絶対に押さえてほしいのは、「len を必ず使う」ことです。

in.read(buffer) は、バッファサイズぴったり読めるとは限らず、 実際に読めたバイト数(len)を返します。

zipOut.write(buffer) としてしまうと、 前回の残りやゴミまで書いてしまい、ZIP の中身が壊れます。

「読み込めた分だけ書く」——この基本を体に染み込ませておくと、 I/O 周りのバグが一気に減ります。

重要ポイント4:ストリームのライフサイクル管理

再帰zipでは、次の三種類のストリームが登場します。

  • ZIP 全体を書き出すための ZipOutputStream
  • ZIP ファイルそのものの OutputStream
  • 各ファイルを読むための InputStream

それぞれを try-with-resources で確実に閉じていることが、 長期運用での安定性に直結します。

特に、各ファイルの InputStream を内側で try-with-resources にしているのがポイントです。 これを忘れると、「ZIP は閉じているのに、個々のファイルハンドルが開きっぱなし」という状態になり、 ファイル数が多いときにハンドル枯渇で障害になります。

「誰がどのストリームを閉じるか」を意識して設計する—— これはセキュリティというより“運用の安全性”の話ですが、業務ではかなり重要です。

再帰zipユーティリティの実務的な使いどころ

バックアップ・エクスポート・一括ダウンロード

再帰zipユーティリティがあると、次のような処理が簡単に書けます。

  • 日次バッチで logs/ 配下を丸ごと ZIP にして保存する
  • 管理画面から「このプロジェクトフォルダを ZIP でダウンロード」させる
  • 設定ファイル群を ZIP にして別環境に移す

どれも、「どのファイルを対象にするか」をコードで細かく列挙するのではなく、 「ベースディレクトリを決めて、その配下を全部 ZIP にする」という形にできるので、 保守性が高くなります。

セキュリティ・運用の視点で見た再帰zip

「どこを ZIP にしてよいか」を決めておく

再帰zipは、「指定したディレクトリ配下を全部まとめる」ので、 機密情報や個人情報を含むファイルをうっかり一緒に入れてしまう危険があります。

業務では、次のようなルールを決めておくと安全です。

  • ZIP 対象にするベースディレクトリを限定する
  • ユーザーに渡す ZIP には、特定のサブディレクトリだけを含める
  • ZIP に含めたファイル一覧をログに残す

ユーティリティ側で「対象ディレクトリを固定する」「特定拡張子だけを対象にする」といった制御を入れておくと、 業務コード側のセキュリティ負担を減らせます。

まとめ:再帰zipユーティリティで身につけてほしい感覚

再帰zipユーティリティは、「ディレクトリ配下を丸ごと ZIP にする」ための道具ですが、 その裏には次のような大事な感覚が詰まっています。

  • ベースディレクトリからの相対パスをエントリ名にして、構造をきれいに保つこと
  • ZIP の中のパスは / 区切りに統一して、環境差をなくすこと
  • 部分読み込み・部分書き込みを徹底して、データ破壊を防ぐこと
  • ストリームのライフサイクルを意識して、リソースリークを防ぐこと
  • 「どこを ZIP にしてよいか」を設計で縛り、機密情報の混入を防ぐこと

もし今、あなたのプロジェクトで、 「バックアップやエクスポートが手作業やバラバラなスクリプトに頼っている」なら、 この再帰zipユーティリティを軸に、ファイル周りのフローを整えてみると、 運用もセキュリティも、かなり楽になります。

タイトルとURLをコピーしました