🦁

Cloudflare ImagesにNode.jsからファイルアップロード

2022/09/23に公開

はじめに

Cloudflare ImagesにAPIを使ってファイルアップロードするときは、
multipart/form-dataを送る必要があります。

シェルで実行する場合は、こんな風にかなりシンプルでOK

curl -X POST \
  -F file=@./image.png \
  -F 'requireSignedURLs=true' \
  -F 'metadata={"key": "value"}' \
  -H "Authorization: Bearer ${API_TOKEN}" \
  https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/images/v1

しかしNode.jsでやろうとするとけっこう面倒くさかったので、
メモに残しておきます。

TL;DR

ファイルアップロードの場合

const axios = require('axios')
const fs = require('fs')
const FormData = require('form-data')

const main = async () => {
  try {
    const form = new FormData()
    form.append('requireSignedURLs', 'true')
    form.append('metadata', JSON.stringify({ key: 'value' }))

    // Cloudflare Imagesでは [jpeg, png, webp, gif, svg] に対応
    const filepath = 'image.png'
    const buffer = fs.readFileSync(filepath)
    form.append('file', buffer, { filename: filepath }) // mimetypeは指定不要(FormDataが自動判別)

    const { data } = await axios(`https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/images/v1`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
      data: form,
    })
    console.log(data)
  } catch (e) {
    console.error(e.response?.data ?? e)
  }
}

main()

URLアップロードの場合

const axios = require('axios')
const fs = require('fs')
const FormData = require('form-data')

const main = async () => {
  try {
    const form = new FormData()
    form.append('requireSignedURLs', 'true')
    form.append('metadata', JSON.stringify({ key: 'value' }))

    // Cloudflare Imagesでは [jpeg, png, webp, gif, svg] に対応
    const url = 'https://example.com/image.png'
    form.append('url', url) // filenameは指定不要

    const { data } = await axios(`https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/images/v1`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
      data: form,
    })
    console.log(data)
  } catch (e) {
    console.error(e.response?.data ?? e)
  }
}

main()

詳解

Cloudflare APIの基本

ファイルアップロードのドキュメントはこちら

APIトークンは、この画面から作成。
アカウントIDも右端で確認可能(キャプチャではボカシ入れてます)
それぞれ環境変数 ACCOUNT_ID API_TOKEN に入れておきましょう。

ライブラリform-data

multipartで送れてない場合は、Cloudflareからこんなエラーが返ってきます。

ERROR 5415: Images must be uploaded as a form, not as raw image data. Please use multipart/form-data format

ブラウザのフォームからファイル添付したときと同じ状態をNode.js上で作る必要があって、
ライブラリform-dataを使って実現できます。まずはインストール。

yarn add form-data

https://github.com/form-data/form-data

const form = new FormData()
form.append('requireSignedURLs', 'true')
form.append('metadata', JSON.stringify({ key: 'value' }))

FormDataインスタンスを作って、APIの仕様にそってデータを何度もappendする。
(なぜコンストラクタに渡せないのだろうか・・・)

ファイルアップロードの場合

const filepath = 'image.png'
const buffer = fs.readFileSync(filepath)
form.append('file', buffer, { filename: filepath }) // mimetypeは指定不要(自動判別)

Bufferオブジェクトを作ってfileプロパティにappendすればOK。
第3引数のオプションにfilenameを指定しないと、ファイル名がブランクで飛んでいきます。
ローカルのファイル名と別名をつけ直してももちろんOK

filenameと同列にcontentTypeを指定することもできるのですが、
FormDataが最適なコンテンツタイプを自動判別してくれるので、明記しなくてもOK

URLアップロードの場合

const url = 'https://example.com/image.png'
form.append('url', url) // filenameは指定不要

URLの文字列をurlプロパティにappendすればOK。
こちらはfilenameも自動判別で、指定しても無視されます。
URLアップロードの場合は、実際のファイル名が変えられないということなんですね。

ちなみにurlfileを同時に指定すると、こんなエラーが起きます。

ERROR 5400: Bad request: file and url fields are mutually exclusive

リクエストボディにFormDataを指定

あとは、上で作ったFormDataオブジェクトをリクエストボディに乗せてあげればOK。
以下はaxiosでの例です。

const { data } = await axios(`https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/images/v1`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
  data: form,
})

ちなみにaxiosの場合、以前はform.getHeaders()をヘッダーに明記してあげないと、
multipart/form-dataのリクエストになってくれなかったのですが、
v0.27以降は省略可能になりました〜。
(というのをこの記事書いてる途中に発見して、別途Qiitaに記事を書きましたw

コード全体

const axios = require('axios')
const fs = require('fs')
const FormData = require('form-data')

const main = async () => {
  try {
    const form = new FormData()
    form.append('requireSignedURLs', 'true')
    form.append('metadata', JSON.stringify({ key: 'value' }))

    // Cloudflare Imagesでは [jpeg, png, webp, gif, svg] に対応
    // ファイルアップロードの場合
    const filepath = 'image.png'
    const buffer = fs.readFileSync(filepath)
    form.append('file', buffer, { filename: filepath }) // mimetypeは指定不要(FormDataが自動判別)

        // URLアップロードの場合
        // const url = 'https://example.com/image.png'
    // form.append('url', url) // filenameは指定不要

    const { data } = await axios(`https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/images/v1`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
      data: form,
    })
    console.log(data)
  } catch (e) {
    console.error(e.response?.data ?? e)
  }
}

main()

catch内でゴニョゴニョやってますが、
axiosのエラーとその他エラーで表示内容を切り替えているということです。
axiosのエラーオブジェクトはめちゃくちゃ長くて、全部出すと読めたもんじゃないのでw

ではまた!

参考文献

Nodeでmultipart/form-dataを送る
Node.js上からmultipart/form-data形式でHTTPリクエストをする
axios v0.27からmultipart/form-dataが簡単に送れるようになった

Discussion