react-jsonschema-formにて、単一ファイル・複数ファイルのアップロードを試してみた

これは「react-jsonschema-formのカレンダー | Advent Calendar 2023 - Qiita」の5日目の記事です。

この記事では、react-jsonschema-form (RJSF) における、単一ファイル・複数ファイルのアップロードを試してみます。

 
目次

 

環境

  • react-jsonschema-form 5.15.0
  • React 18.2.0
  • React Router 6.20.1

 

RJSFにて input="file" な項目を定義するには

このアドベントカレンダーの2日目の記事でふれていますが、

singleFile: {
  type: "string",
  format: "data-url"
},

のように、

  • type: "string"
  • format: "data-url"

なpropertiesを定義します。

 
RJSFでは、ファイルアップロードについて

  • 単一ファイル
  • 複数ファイル

の両方に対応しているため、次はそれぞれを試してみます。

 
なお、アップロードするファイルですが、テキストファイルと画像ファイルを用意します。

テキストファイルは以下の文字列が含まれる、拡張子 txt ファイルとします。

abc

 

単一ファイルのアップロード

ファイルアップロード後にformDataの値を確認する

RJSFのFormにある onSubmit propsに割り当てる関数にて、 formData の値を確認する処理を記載します。

import {RJSFSchema} from "@rjsf/utils";
import Form from "@rjsf/mui";  // MUI version
import validator from "@rjsf/validator-ajv8";

export const SingleFileUpload = () => {
  const schema: RJSFSchema = {
    title: "Single File Upload",
    type: "object",
    required: [],
    properties: {
      singleFile: {
        type: "string",
        format: "data-url",
      }
    }
  }

  // onSubmitの引数の説明は以下
  // https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props#onsubmit
  const onSubmit = async ({formData}, _event) => {
    console.log(formData)

    const singleFile = formData.singleFile
    console.log(singleFile)
  }

  return (
    <div style={{width: "400px"}}>
      <Form
        schema={schema}
        validator={validator}
        onSubmit={onSubmit}
      />
    </div>
  )
}

 
動かしてみます。

ダイアログでファイルを選択すると、以下のように表示されました。

 
続いてsubmitすると、console.logには

{singleFile: 'data:text/plain;name=2023_1205_upload.txt;base64,YWJj'}

data:text/plain;name=2023_1205_upload.txt;base64,YWJj

が表示されました。

 

dataUrlとは

上記でみたように、 formDataに dataUrl が含まれていました。

dataUrl については、MDNでは以下の解説がありました。

データ URL は data: スキームが先頭についた URL で、小さなファイルをインラインで文書に埋め込むことができます。

(略)

データ URL は接頭辞 (data:)、データの種類を示す MIME タイプ、テキストではないデータである場合のオプションである base64 トークン、データ自体の 4 つの部品で構成されます。

データ URL - HTTP | MDN

 

dataUrlからファイルの中身を取り出す

RJSFの onSubmit で送信した内容はデータ自体に埋め込まれています。

そこで、以下のMDNの内容を参考に、データをデコードして読めるようにしてみます。
Converting arbitrary binary data | Base64 - MDN Web Docs Glossary: Definitions of Web-related terms | MDN

const onSubmit = async ({formData}, _event) => {
  console.log(formData)

  const singleFile = formData.singleFile

  // 追加
  const data = await dataUrlToBytes(singleFile)
  console.log(data)
}

const dataUrlToBytes = async (dataUrl) => {
  // ファイルの中身を取り出す
  // https://developer.mozilla.org/en-US/docs/Glossary/Base64
  const r = await fetch(dataUrl)
  const a = new Uint8Array(await r.arrayBuffer())
  return new TextDecoder().decode(a)
}

 
動かしてみると、consoleに abc が表示されました。

ファイルの中身が取り出せたようです。

 
なお、画像ファイルをアップロードした場合は、以下のような人間には読めないものが表示されました。

 

dataUrlを元に、ファイルをブラウザ上に表示する

dataUrlに含まれるデータの中身ですが、

  • img
  • iframe

などのHTMLタグを使うことでブラウザ上に表示できます。

 
そこで、今回は iframe を使い、

  • formDataに含まれるファイルの中身を useState に保存する
  • コンポーネントでは useState で保存している中身を表示する

という実装を試してみます。

 
まずはstateを保存する部分です。

// stateを保持
const [data, setData] = useState("")

const onSubmit = async ({formData}, _event) => {
  const singleFile = formData.singleFile

  // stateに保存
  setData(singleFile)
}

 
あとは、iframe を使って、stateに保存されている内容を表示します。

return (
  <div style={{width: "400px"}}>
    {data && (<div><iframe src={data} /></div>)}
    {data && (<div><a href={data} download={getFileName(data)}>ダウンロードリンク</a></div>)}

    <Form
      schema={schema}
      validator={validator}
      onSubmit={onSubmit}
    />
  </div>
)

 
実際に動かしてみます。

まずはテキストファイルをアップロードした結果です。ファイルの中身がiframeで表示されています。

 
続いて画像ファイルをアップロードします。こちらもiframeで表示されました。

 

dataUrlを元に、アップロードしたファイルをダウンロードできるようにする

dataUrlのデータをダウンロードすることも試してみます。

今回は以下とします。

  • iframe で表示するときに使ったコンポーネントに対し、 a タグを使ったダウンロードリンクも追加
  • stateで保存している dataa タグの src に設定
  • ダウンロード時のファイル名は download 属性にて指定
    • RJSFの場合、ファイル名は dataUrl に含まれるため、そこから取り出す関数 getFileName を用意

 
方針に従った実装はこちら。

// ファイル名を取得する関数
const getFileName = (data: string) => {
  return data.split(";")[1].replace("name=", "")
}

return (
  <div style={{width: "400px"}}>
    {data && (<div><iframe src={data} /></div>)}
    {data && (<div><a href={data} download={getFileName(data)}>ダウンロードリンク</a></div>)}

    <Form
      schema={schema}
      validator={validator}
      onSubmit={onSubmit}
    />
  </div>
)

 
ブラウザでファイルをsubmitしたところ、ダウンロードリンクが表示されました。

 
ダウンロードリンクをクリックすると、ダウンロード用のダイアログが開き、ファイル名が設定されていました。

 
そのままダウンロードを進めると、指定のディレクトリにファイルが保存されました。

 
以上で単数ファイルの挙動を確認できたため、次は複数ファイルの挙動を確認してみます。

 

複数ファイルアップロード

アップロード後のデータを確認する

続いて、複数ファイルアップロードを試してみます。

単数のときとの違いは、 propertiestype: "array" を指定していることです。

import {RJSFSchema} from "@rjsf/utils";
import Form from "@rjsf/mui";  // MUI version
import validator from "@rjsf/validator-ajv8";

export const MultipleFileUpload = () => {
  const schema: RJSFSchema = {
    title: "Multiple File Upload",
    type: "object",
    required: [],
    properties: {
      multipleFile: {
        type: "array",
        items: {
          type: "string",
          format: "data-url"
        }
      }
    }
  }

  // onSubmitの引数の説明は以下
  // https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/form-props#onsubmit
  const onSubmit = async ({formData}, _event) => {
    console.log(formData)

    const f = formData.singleFile
    console.log(f)
  }

  return (
    <div style={{width: "400px"}}>
      <Form
        schema={schema}
        validator={validator}
        onSubmit={onSubmit}
      />
    </div>
  )
}

 
では、複数ファイルをアップロードして動かしてみます。

Windowsの場合、ファイルを選択するダイアログでは1つのファイルのみ選択できます。そのため、ファイルの選択を繰り返すことで、複数ファイルアップロードができるようになります。

ちなみに、RJSFのデフォルトでは、選択したファイルを削除するようなボタンは見当たりませんでした。

 
続いて、submitしてconsoleを見ると、配列でdataUrlが生成されていることが分かりました。

 
なお、ファイルの中身を取り出したり、表示したりするのは単一ファイルと同じなため、ここでは省略します。

 

ソースコード

Githubにあげました。
https://github.com/thinkAmi-sandbox/rjsf_advent_calendar_2023

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/rjsf_advent_calendar_2023/pull/4