加具留矢流余

かぐるやるよ

移転しました。

約3秒後に自動的にリダイレクトします。

Google Drive APIでファイルをアップロード - Multipart/Related

前回に引き続きGoogleDriveAPIのお話です。 今回はファイルの新規アップロードについてです。

createエンドポイント

ファイルを新規にアップロードするにはcreateエンドポイントを使用します。

https://developers.google.com/drive/api/v3/reference/files/create

このエンドポイントが曲者で、アップロード時に3つのアップロードタイプから一つを選んで明示的に指定する必要があります。

  • media - ファイルのみをアップロード
  • multipart - ファイルとメタデータを同時にアップロード
  • resumable - 複数回のリクエストでファイルとメタデータをアップロード

現実問題としてメタデータ無しでファイルをアップロードしたいという局面は殆ど無いと思われます。Google Driveではフォルダの階層構造をファイルのメタデータで管理しているため、メタデータ抜きでは親ディレクトリの指定すらできません。

ということで多くの場合においてアップロードタイプとしてmultipartかresumableを指定することになるでしょう。複数回リクエストを投げるよりは一回のリクエストで片付けてしまいたいので、自分の中でmultipartを使ってみようということになりました。ところがこのmultipart、RFC 2387というプロトコルに則ってリクエストを作成する必要があります。日本語の文献もほぼ無いということで素直にRFCを流し読みしました。

RFC 2387 The MIME Multipart/Related Content-type

tools.ietf.org

複数の異なるMimeTypeのデータをまとめてアップロードするための「Multipart/Related」というMimeTypeについてまとめてたものです。あまりしっかりと読めているわけではないですが、要点は以下の通りとなっています。

  • Content-TypeでMultipart/Relatedを指定し、同時にBoundary(境界文字列)も指定する。
  • リクエストボディは"--"+Boundaryで区切って、異なるデータのリクエストボディを指定する。
  • リクエストの終わりは"--"+Boundary+"--"で締める

これだけだと抽象的で少し分かりづらいですが、RFCの原本に載っている例がわかりやすいです。

Content-Type: Multipart/Related; boundary=example-1 start="950120.aaCC@XIson.com"; type="Application/X-FixedRecord" start-info="-o ps"

 --example-1
 Content-Type: Application/X-FixedRecord
 Content-ID: <950120.aaCC@XIson.com>

 25
 10
 34
 10
 25
 21
 26
 10
 --example-1
 Content-Type: Application/octet-stream
 Content-Description: The fixed length records
 Content-Transfer-Encoding: base64
 Content-ID: <950120.aaCB@XIson.com>

 T2xkIE1hY0RvbmFsZCBoYWQgYSBmYXJtCkUgSS
 BFIEkgTwpBbmQgb24gaGlzIGZhcm0gaGUgaGFk
 IHNvbWUgZHVja3MKRSBJIEUgSSBPCldpdGggYS
 BxdWFjayBxdWFjayBoZXJlLAphIHF1YWNrIHF1
 YWNrIHRoZXJlLApldmVyeSB3aGVyZSBhIHF1YW
 NrIHF1YWNrCkUgSSBFIEkgTwo=

 --example-1--

HTTP本体のリクエストヘッダにはMultipart/Relatedを指定しています。startなどはどのデータから読み込めばいいかを示す情報のようです(それっぽいことが書いてありましたが良く理解できませんでした)。リクエストボディの各セクションでは、それぞれのデータに対して改めてヘッダを定義しています。

javascriptでの実装

Multipart/Relatedのリクエストボディを生成する関数を作成した。この関数でエンコードした文字列をリクエストボディにして、リクエストヘッダにはMultipart/Relatedを指定すれば

/**
 * 引数で指定したオブジェクトをMultipart/Relatedのリクエストボディに変換
 * 以下のような配列を第一引数として指定する必要がある
 * e.g)
 * let objects = [
 *   { header: ['Content-Type': 'text/plane'], body: '内容1' },
 *   { header: ['Content-Type': 'text/plane'], body: '内容2' },
 * ]
 * 
 * @param {Array} エンコードしたいオブジェクトのリスト 
 * @param {String} 境界文字列
 * @returns {String} エンコード済みの文字列
 */
function encodeMutipart(objects, separator) {
  let encoded = '';
  for (let o of objects) {
    encoded += `--${separator}`;
    encoded += '\n';
    Object.keys(o.headers).forEach((key) => {
      encoded += key;
      encoded += ': ';
      encoded += o.headers[key];
      encoded += '\n';
    });
    encoded += '\n';
    if (typeof o.body === 'object') {
      encoded += JSON.stringify(o.body);
    } else if (typeof o.body === 'string') {
      encoded += o.body;
    }
    encoded += '\n';
  }
  encoded += `--${separator}--`;
  return encoded;
}

mikebird28.hatenablog.jp