🗂

フロントエンドから署名付きURLを使ってGCS上のファイルをローカルに保存する

2022/04/26に公開

はじめに

GraphQLを使用している関係でファイルをダウンロードする際には

  1. 署名付きURLの生成をBEにリクエスト
  2. 受け取った署名付きURLを元にGCSのファイルにアクセスしてファイルを取得

とい処理が必要になるのですが、署名付きURLを使用してGCS上のファイルをローカルに保存するまでにいくつか詰まったので参考までに紹介したいと思います。

最終的なコードは下記にpushしてあります。

https://github.com/KazukiHayase/gcs-signed-url-download-sample

前提

  • React 17.0.2 (Next.js 12.0.9)
  • コードベースでの署名付きURLの生成方法については触れないです

実装

署名付きURL生成API

今回は受け取った署名付きURLを元にファイルをダウンロードする部分フォーカスしているので、文字列を返すだけの簡単なエンドポイントを用意します。

import { NextApiRequest, NextApiResponse } from "next";

const handler = (_req: NextApiRequest, res: NextApiResponse) => {
  const signedDownloadUrl = ""; // gsutilで生成した署名付きURLを代入
  res.status(200).json({ signedDownloadUrl });
};

export default handler;

gsutilコマンドなどで生成したURLを上記に代入することで、署名付きURLを生成するAPIの代わりとして使用します。

gsutil signurl -m GET -d 12h -c text/csv -u gs://sample-bucket/sample.csv

V4 signing process with Cloud Storage tools | Google Cloud

ダウンロードする関数

次に署名付きURLを元にファイルをローカルに保存する関数を実装していきます。

import saveAs from "file-saver";

export const downloadFile = async (
  signedDownloadUrl: string,
  contentType: string,
  fileName: string
): Promise<void> => {
  const res = await fetch(signedDownloadUrl, {
    method: "GET",
    headers: { "Content-Type": contentType },
  });
  if (!res.ok) {
    return Promise.reject(new Error("Fail to download file from GCS"));
  }
  const blob = await res.blob();

  saveAs(blob, fileName);
};

受け取った署名付きURLに対してGETリクエストを送り、そのレスポンスをblobに変換後、file-saverというライブラリのsaveAsという関数でローカルに保存しています。

最初はURLに対してGETリクエストを送ったら自動的にローカルに保存されたりするのかなと思っていたのですが、そうではなくあくまでリクエストで受け取ったファイルをローカルに保存するための処理は別で必要でした。

それが上記のsaveAsの部分に当たるのですが、この関数では下記のようにjsでaタグの生成、クリックを行うことでファイルをローカルへの保存するようになっています。

var a = document.createElement('a')
a.href = blob
a.target = '_blank'
setTimeout(function () { click(a) })

https://github.com/eligrey/FileSaver.js

ページ実装

最後にAPIリクエスト部分と上記の実装した関数のつなぎこみを行うページを実装すれば完成です!

const IndexPage = () => {
  const handleClick: MouseEventHandler<HTMLButtonElement> = async () => {
    try {
      const res = await fetch("api/signedUrl", { method: "GET" });
      const data = await res.json();
      const signedDownloadUrl = data.signedDownloadUrl;
      await downloadFile(signedDownloadUrl, "text/csv", "sample.csv");
    } catch (e) {
      console.log(e);
    }
  };

  return (
    <div>
      <h1>GCS signed download url sample</h1>
      <button onClick={handleClick}>ファイルをダウンロード</button>
    </div>
  );
};

詰まったポイント

上記の実装内容以外にもいくつか詰まったポイントがあったので紹介します。

リクエストヘッダーでContent-Typeを指定する

署名付きURLにリクエストを送る際のヘッダーに、URL生成時に指定したContent-Typeを含めないとエラーになってしまいます。(URL生成時に指定していない場合は不要)

上記の例だと

gsutil signurl -m GET -d 12h -c text/csv -u gs://sample-bucket/sample.csv

-cオプションでContent-Typeにtext/csvと指定しているので、署名付きURLにリクエストする際のContent-Typeヘッダーにはtext/csvを指定する必要があります。

CORS設定

フロントからリクエストするためにはバケット側にCORSの設定が必要です。

参考までに上記の実装をローカルで動かすためのCORS設定は下記のようになります。

[
    {
      "origin": ["http://localhost:3000"],
      "method": ["GET"],
      "responseHeader": ["Content-Type"],
      "maxAgeSeconds": 3600
    }
]

https://cloud.google.com/storage/docs/configuring-cors

まとめ

GaphQLを採用した際にはRESTとは異なる方法でファイルを扱う必要があるので、その方法の一つとして参考にしてみてください!

株式会社BuySell Technologies

Discussion