Google Apps ScriptでAsanaにファイルアップロードしようとして苦戦した話

最近Asana APIを触る機会があり、API自体は特に難しいこととかなくて特筆することはないんだけれども、ファイルをアップロードしようとしたら日本語のファイル名が文字化けする問題が発生して、無事沼にハマったので忘れないように記事にしておこうと思う。

(調べても全然解決できる手段が出てこなかったんですよね、、、)

利用するAPIエンドポイント

ファイルのアップロードに利用するのは「Upload an attachment」のAPIエンドポイント。

APIドキュメント内にある実装例のコードを見てもらってもわかる通り、ファイルアップロードするだけならなんら難しいことはない。特にGASを利用する場合はURLFetchの機能で勝手にmultipart/form-dataとして扱ってくれるので、本当に何も難しいことはない。

参考:https://qiita.com/asmasa/items/4fd7b5f3f3d1a33984b3

以下、修正前に実装したmultipart/form-dataとかしてないコード

/**
 * Asana APIを扱うクラス
 */
class Asana {
  constructor () {
    this.endpoint_base = GLOBAL.ASANA.ENDPOINT_BASE;
    this.workspace_id = GLOBAL.ASANA.WORKSPACE_ID;
  }
  
  //------- < 中略 > -------

 /**
   * ファイルのアップロード
   * @param {string} taskGid : タスクID(parent)
   * @param {file} fileData : アップロードするファイル
   */
  uploadFile (taskGid , fileData) {
    const endpoint = this.endpoint_base + '/attachments';
    const filePayload = {
      'file' : fileData,
      'parent' : taskGid
    };
    const options = {
      'method' : 'post',
      'headers' : { 
        'Authorization': 'Bearer ' + GLOBAL.ASANA.API_TOKEN,
        'accept': 'application/json'
      },
      'payload' : filePayload
    }
    const execUpload = UrlFetchApp.fetch(endpoint, options);
    return JSON.parse(execUpload.getContentText());
  }
}

日本語のファイル名だと文字化けする

「めっちゃイージーじゃん」と余裕ぶっこいてたら、「いくつかのファイル名が文字化けしてアップロードされています」と報告がきた。日本語のファイル名が文字化けしてしまうことは割とよくあるから驚きはなかったけれど、GASからアップロードするときってどうやって対処したらいいんだ?

文字化けした日本語のファイル名

結局は自力でmultipart/form-dataを作らないといけないみたい

日本語のファイル名が文字化けしてしまう原因はShift-jisの文字コードが悪さをしているせいなのですけど、これを解消するためにはファイル側の文字コードをエンコードして、UTF-8のファイル名に変換しないといけない。

GASでやるにはどうしたらいいのか?
調べてみたところ、日本語のファイル名をエンコードしてmultipart/form-dataを作成するところまでわりかしたくさん見つかった。
しかしそれらを参考に実装してみても、一生400エラーが返ってくる。何を書き換えても400エラーが返ってくる。

まずベースとして参考にさせていただいのは以下の記事。
multipart/form-dataでファイルのみをPOSTするならたぶんこれだけで十分。

しかし、Asana APIの場合には、ファイル + parentの情報をペイロードに設定してPOSTする必要がある。これがマジで調べてもなかなか出てこない。

Asanaのフォーラムにあった問い合わせ通りに実装してみても、ここで紹介されているエンドポイントがそもそも404エラーで返ってきてしまい、現在は存在しない模様。パラメータはファイルのみなのであまり参考にならなかった。

そもそもmultipart/form-dataとはどんな仕組みなのか?

ここまで来るともはや調べて答えが出てくる問題ではないことがわかったので、GAS云々ではなくmultipart/form-dataをちゃんと理解して、自力で答えを見つけ出す方が早そうだと思った。

とても勉強になった記事がこちら
ファイル以外のパラメータの設定方法から、フォームデータの記載フォーマットまでだいたい理解できた。

こちらの記事で紹介されているフォームデータの形に倣って修正してみてGASコードが以下。

/**
 * Asana APIを扱うクラス
 */
class Asana {
  constructor () {
    this.endpoint_base = GLOBAL.ASANA.ENDPOINT_BASE;
    this.workspace_id = GLOBAL.ASANA.WORKSPACE_ID;
  }
  
  //------- < 中略 > -------

 /**
   * ファイルのアップロード
   * @param {string} taskGid : タスクID(parent)
   * @param {file} fileData : アップロードするファイル
   */
  uploadFile (taskGid , fileData) {
    const boundary = 'boundary'; //フォームデータの境界となる文字
    const endpoint = this.endpoint_base + '/attachments';
    const filePayload = this.parseFileData( boundary, fileData, taskGid)
    const options = {
      'method' : 'post',
      'headers' : { 
        'Authorization': 'Bearer ' + GLOBAL.ASANA.API_TOKEN,
        'accept': 'application/json',
        'content-type' : 'multipart/form-data; boundary=' + boundary
      },
      'payload' : filePayload
    }
    const execUpload = UrlFetchApp.fetch(endpoint, options);
    return JSON.parse(execUpload.getContentText());
  }

  /**
   * ファイルリクエストのフォームデータを生成
   * @param {string} boundary : フォームの区切り文字
   * @param {file} file : ファイルデータ
   * @param {string} taskId : parentに指定するタスクID
   * @return {array} - リクエストボディに設定するフォームデータ 
   */
  parseFileData (boundary, file, taskId) {
    //ファイル名の変換
    const filename = file.getName();
    const encodedName = encodeURIComponent(filename);
    //リクエストボディを生成(parent, file)
    const blobByte = Utilities.newBlob(
      '--' + boundary + '\r\n'
      + 'Content-Disposition: form-data; name="parent"\r\n\r\n' //parent = task_id
      + taskId + '\r\n'
      + '--' + boundary + '\r\n' //フォームの区切りを付与
      //ファイルデータ ※日本語ファイル名が文字化けしないようにエンコード対応 + UTF-8を指定
      + 'Content-Disposition: form-data; name="file"; filename="' + encodedName + `"; filename*=UTF-8''${encodedName}\r\n`
      + 'Content-Type: ' + file.getContentType() + '\r\n\r\n'
    ).getBytes().concat(
      file.getBytes()       //ファイルの情報を付加
    ).concat(
      Utilities.newBlob('\r\n--' + boundary + '--\r\n').getBytes() //最後の区切りを付加
    );
    return blobByte;
  }
}

修正した結果

このコードに修正して実行してみたところ問題なく日本語のファイル名でファイルがアップロードされた。日本語のファイル名も半角英数のファイル名もどちらもいけてそう。ファイルの中身もちゃんとPDFファイルのままだった。

長年よくわからないままなんとなーく扱っていたAPIでのファイル送信。
とても勉強になった良い機会でした。

この記事が気に入ったらサポートをしてみませんか?