指定したファイルやディレクトリのzip化をやってみる

今日も備忘録です。

## このドキュメントについて
下記2点について記載していきます。

1. zipOutputStreamを利用して、指定したファイルやフォルダをzip化する方法
2. ZipEntryの使い方と注意点


※以下File操作系のクラスやメソッドについては、事前知識として必要
①：FileInputStream/FileOutputStreamクラス
②：Filesクラス
③：Pathクラスなど

## 実行環境

・Java17
・springboot 3.0.2

## 実装(全体の処理の流れ)

```java
    public void zipStream(String rootPath) {
        var path = Paths.get(rootPath);
        var zipFilePath = Paths.get("テストまとめ.zip");
        if (Files.exists(path)) {
            try (var zip = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFilePath.toFile())))) {
                if (Files.isDirectory(path)) {
                    directoryZip(path.getNameCount(), path, zip);
                } else {
                    // 新しいファイル名を指定し、zip中に設定
                    zip.putNextEntry(new ZipEntry(path.getFileName().toString()));
                    zip.write(Files.readAllBytes(path));
                }
            } catch (IOException e) {
                throw new RuntimeException("ファイル操作で問題が発生しました。", e);
            }
        }
    }
```
説明：
①:メソッドの引数で受け取った文字列をzip化したいフォルダ、ファイルのパスとして定義する。zip化したフォルダ、ファイルをどこに配置するかも定義しておく。


②：zip化したいフォルダ、ファイルが存在するかのチェックを入れる。(このチェックを入れることでIOEXceptionを発生させずに済む)


③：ZipOutputStreamで出力ストリームを開き、引数にはBufferedOutputStreamを利用する。バッファメモリを使うことで効率よくデータを出力できるようにしておく。

また、try-with-resorcesを利用して、処理が終わり次第自動的にcloseしてくれるようにしておきます。(メモリ不足で実行できなくなるかもしれないので、書く癖をつける)

④：パスがファイルかディレクトリかで処理を分ける。
ファイル → putNextEntry()でファイル名を指定して、バイトデータを書き込む

ディレクトリ → 別メソッドでファイル数分処理を繰り返す  ※以下↓で説明

<br>

```java
    private void directoryZip(int rootCount, Path path, ZipOutputStream zip) throws IOException {
        Files.list(path).forEach(
                p -> {
                    try {
                        var pathName = p.subpath(rootCount, p.getNameCount());
                        if (Files.isDirectory(p)) {
                            zip.putNextEntry(new ZipEntry(pathName + "/"));
                            directoryZip(rootCount, p, zip);
                        } else {
                            zip.putNextEntry(new ZipEntry(pathName.toString()));
                            zip.write(Files.readAllBytes(p));
                        }
                    } catch (IOException e) {
                        throw new RuntimeException("ファイル操作で問題が発生しました。", e);
                    }
                });
    }
```

①：変数pathはディレクトリのパスなので、パスの配下にあるファイル、フォルダ数分処理を行う
フォルダだった場合は、再度このメソッドを呼び出す仕様。

②：サブフォルダだった場合に、フォルダ名 + "/"をputNextEntry()で設定しておくと、サブフォルダをzipファイルの中に作ってくれる

③：ファイルだった場合は、先ほどの処理と同様の処理

<br>
<br>
これでサブフォルダが複数ある場合でも、同じ構造でzip化できます。

 ## ZipEntryの使い方と注意点
今回ハマった(時間がかかった)ZipEntryの使い方についてです。


最初は指定したフォルダ内にサブフォルダがあれば、同じ階層でzip化できなくて、以下のようになってました。

```
zip化したい対象のフォルダ
test.zip
ー テスト.txt
ー テスト2.txt
ー テストディレクトリ
 ーー テスト3.txt
```

↓こうなっちゃってた (テスト3.txtがテストディレクトリの中に格納されない)

```
ー テスト.txt
ー テスト2.txt
ー テストディレクトリ
ー テスト3.txt
```

その原因が以下のように最初思ってたからです。

```java
zip.putNextEntry(new ZipEntry("フォルダ名"+ "/")); // ← これをやったら次はこのフォルダ配下にファイルが置かれると思ってた
zip.putNextEntry(new ZipEntry("zip内に置きたいファイル名")) // ← これは上記のフォルダの配下に置かれると思ってた
```

解決方法は、以下。
```java
zip.putNextEntry(new ZipEntry("フォルダ名"+ "/")); 
zip.putNextEntry(new ZipEntry("フォルダ名"+ "/" + "zip内に置きたいファイル名")) 
```
ZipEntryの引数には、ファイル名だけではなく、zipファイル内のどこにファイルを置きたいのかというパスを書く必要がありました。
これでファイル名もちゃんとついて解決しました。


補足：
zipを解凍する際のzipInputStreamの扱いについて

zipInputStream.getNextEntry()で取得できるzipEntryでは、
zipEntry.getName()は、zipフォルダ内から対象ファイルまでのパスが取得できる。
(ファイル名だけではなく)

zipInputStream.getNextEntry()後に、バイナリーデータを取得するメソッドを利用する場合(read()やreadAllBytes())、
取得できるデータは対象ファイルのみ。zip内のファイルデータ全てが取得できる訳ではない。


## まとめ
今度参画する案件で、zip化したファイルをS3に格納する処理があるので、事前学習としてやってみました。
もっといい方法がある！という方がいれば、教えて欲しいです。。