ReactでJSZipを使用してファイルのアップロードと内容表示機能を実装する

こんばんは。

今日は「ReactでJSZipを使用したファイルアップロード」について説明いたします。

# 必要なパッケージのインストール

```bash
npm install jszip
```

# Reactコンポーネントの作成

`JSZip`ライブラリを使用して、アップロードされた`ZIP`ファイルの内容を読み取り、表示するコンポーネントを作成します。

```React
import React, { useState } from 'react';
import JSZip from 'jszip';

function ZipFileUploader() {
  const [filesList, setFilesList] = useState([]);

  const handleFileUpload = async (event) => {
    const file = event.target.files[0];
    if (file) {
      const zip = new JSZip();
      try {
        const content = await file.arrayBuffer();
        const loadedZip = await zip.loadAsync(content);
        const files = [];
        loadedZip.forEach((relativePath, zipEntry) => {
          files.push(relativePath);
        });
        setFilesList(files);
      } catch (error) {
        console.error('ZIPファイルの読み込み中にエラーが発生しました:', error);
      }
    }
  };

  return (
    <div>
      <h1>ZIPファイルのアップロードと内容表示</h1>
      <input type="file" accept=".zip" onChange={handleFileUpload} />
      <h2>ファイル一覧:</h2>
      <ul>
        {filesList.map((fileName, index) => (
          <li key={index}>{fileName}</li>
        ))}
      </ul>
    </div>
  );
}

export default ZipFileUploader;
```

# コードの説明

- ファイルアップロードハンドラー (`handleFileUpload`)
    - ユーザーがファイルを選択すると呼び出されます。
    - 選択されたファイルが存在するか確認します。
    - `JSZip`のインスタンスを作成し、ファイルの内容を`arrayBuffer`として読み込みます。
    - `zip.loadAsync`を使用して、ZIPファイルの内容を非同期的にロードします。
    - `loadedZip.forEach`を使用して、`ZIP`内の各ファイルにアクセスし、ファイル名を配列に保存します。
    - `setFilesList`を使用して状態を更新し、画面にファイル一覧を表示します。
- ファイル一覧の表示
    - `filesList`の状態をループして、ファイル名を<ul>リストとして出力します。


# 追加機能の実装

## ファイル内容の表示

テキストファイルや画像の場合、ファイルの内容を読み込んで画面に表示できます。
`zipEntry.async("text")`または`zipEntry.async("base64")`を使用してファイル内容を取得します。

```React
// テキストファイルの内容を読み込む例
const handleFileUpload = async (event) => {
  // 前述のコードと同じ
  const filesContent = [];
  loadedZip.forEach(async (relativePath, zipEntry) => {
    if (!zipEntry.dir) {
      const content = await zipEntry.async('text');
      filesContent.push({ name: relativePath, content });
      setFilesList([...filesContent]);
    }
  });
};

// レンダリング部分でファイル内容も表示
<ul>
  {filesList.map((file, index) => (
    <li key={index}>
      <h3>{file.name}</h3>
      <pre>{file.content}</pre>
    </li>
  ))}
</ul>
```

## 画像ファイルの表示

画像の場合、`base64`または`blob`形式で読み込み、`<img>`タグに挿入できます。

```React
// 画像ファイルを読み込む例
if (zipEntry.name.match(/\.(png|jpe?g|gif)$/)) {
  const content = await zipEntry.async('base64');
  filesContent.push({
    name: relativePath,
    content: `data:image/*;base64,${content}`,
  });
}

// レンダリング部分で画像を表示
{filesList.map((file, index) => (
  <div key={index}>
    <h3>{file.name}</h3>
    {file.content.startsWith('data:image') ? (
      <img src={file.content} alt={file.name} />
    ) : (
      <pre>{file.content}</pre>
    )}
  </div>
))}
```

# 注意事項

- 非同期処理
    - `forEach`内で非同期関数を使用する場合、`Promise.all`を使用して全てのファイルを処理した後に状態を更新するのが良いです。
- 大容量ファイルの処理
    - 大きなファイルや多数のファイルを処理する際は、パフォーマンスに注意が必要です。
- セキュリティ問題
    - アップロードされたファイルの内容をそのまま表示すると、`XSS`攻撃のリスクがあるため、内容を適切にエスケープするか、信頼できるファイルのみを処理する必要があります。

# 全体のコード

```React
import React, { useState } from 'react';
import JSZip from 'jszip';

function ZipFileUploader() {
  const [filesList, setFilesList] = useState([]);

  const handleFileUpload = async (event) => {
    const file = event.target.files[0];
    if (file) {
      const zip = new JSZip();
      try {
        const content = await file.arrayBuffer();
        const loadedZip = await zip.loadAsync(content);
        const filesContent = [];

        await Promise.all(
          Object.keys(loadedZip.files).map(async (filename) => {
            const zipEntry = loadedZip.files[filename];
            if (!zipEntry.dir) {
              if (zipEntry.name.match(/\.(png|jpe?g|gif)$/)) {
                const content = await zipEntry.async('base64');
                filesContent.push({
                  name: zipEntry.name,
                  type: 'image',
                  content: `data:image/*;base64,${content}`,
                });
              } else {
                const content = await zipEntry.async('text');
                filesContent.push({
                  name: zipEntry.name,
                  type: 'text',
                  content,
                });
              }
            }
          })
        );

        setFilesList(filesContent);
      } catch (error) {
        console.error('ZIPファイルの読み込み中にエラーが発生しました:', error);
      }
    }
  };

  return (
    <div>
      <h1>ZIPファイルのアップロードと内容表示</h1>
      <input type="file" accept=".zip" onChange={handleFileUpload} />
      <h2>ファイル一覧:</h2>
      {filesList.map((file, index) => (
        <div key={index}>
          <h3>{file.name}</h3>
          {file.type === 'image' ? (
            <img src={file.content} alt={file.name} />
          ) : (
            <pre>{file.content}</pre>
          )}
        </div>
      ))}
    </div>
  );
}

export default ZipFileUploader;
```

上記のコードは、ユーザーがZIPファイルをアップロードすると、その内容を読み込んで画面に表示するReactコンポーネントです。テキストファイルは`<pre>`タグで、画像ファイルは`<img>`タグで表示します。

# 参考事項

- セキュリティ: 実際のアプリケーションでは、アップロードされたファイルに対する厳密な検証とセキュリティ対策が必要です。
- エラーハンドリング: ユーザーに明確なエラーメッセージを提供するために、追加のエラーハンドリングを実装することをお勧めします。
- スタイリング: 必要に応じて、CSSを使用してコンポーネントのデザインを改善できます。

今日は以上です。

ありがとうございました。
よろしくお願いいたします。