vue.js
Firebase

FirebaseのHostingとCloud Functionsを利用してStorageからファイルをダウンロードするデモアプリケーション

概要

以前に投稿した「FirebaseのHostingとCloud Functionsを利用してStorageへファイルをアップロードするデモアプリケーション」という記事で開発したデモアプリケーションにファイルダウンロード機能を追加しました。

ファイルダウンロードの方法は、まずダウンロードするファイル名をgetリクエストのパラメータに指定し、Storageにファイルが存在すればダウンロード用の署名付きURLをレスポンスします。
次に署名付きURLにアクセスしてStorageから直接ファイルをダウンロードします。

今回実装した機能

  1. Cloud FunctionsのHTTP関数でgetリクエストされたファイルの署名付きURLをレスポンスする処理を実装します。
  2. Vue.jsアプリケーションでファイルをダウンロードするフォームを実装します。
  3. ダウンロードURLをHostingのrewrite機能でHTTP関数のエンドポイントへディスパッチします。
  4. 署名付きURLを使ってStorageから直接ファイルをダウンロードします。

前回実装した機能

  • Cloud FunctionsのHTTP関数でpostリクエストされたファイルをStorageに保存する処理を実装します。
  • Vue.jsアプリケーションでファイルをアップロードするフォームを実装します。
  • アップロードURLをHostingのrewrite機能でHTTP関数のエンドポイントへディスパッチします。

構成図

firebase.png

アプリケーション開発の事前準備

以前に投稿した「FirebaseのHostingとCloud Functionsを利用してStorageへファイルをアップロードするデモアプリケーション」と重複する部分が多いので、ファイルダウンロード機能の追加で必要になった作業のみ記載します。

署名付きURLを発行するための設定

署名付きURLの発行に必要な権限はiam.serviceAccounts.signBlobです。この権限を含むロール(役割)を新しく作成するか、この権限を持っている既存のロール(サービスアカウントトークン作成者)を利用してサービスアカウントに割り当てます。
この記事では既存のロール(サービスアカウント トークン作成者)を利用する方法にしました。

iam4.png

また、"Identity and Access Management (IAM) API"を有効にする必要があります。
これらの作業は現時点(2018/05)でFirebase Consoleからは行えないので、GCP Consoleから行います。

GCP Console

GCP Consoleにログインし、Firebaseで作成したプロジェクトを選択します。

役割(ロール)の設定

GCP Consoleの左側メニューより"IAMと管理" → "IAM"を選択するとメンバーが表示されます。
その中で名前が”App Engine default service account”というメンバーの鉛筆アイコンをクリックします。

iam0.png

「別の役割を追加」をクリックし”サービスアカウントトークン作成者”という役割を追加します。

iam1.png

Identity and Access Management (IAM) APIの有効化

左側メニューの"APIとサービス" → "ライブラリ"を選択し、検索フィールドに”Identity and Access Management”と入力します。検索結果にAPIが表示されるのでクリックします。

iam2.png

「有効にする」ボタンをクリックしてAPIを有効にします。

iam3.png

これで署名付きURLを発行する準備は整いました。

StorageにCORSの設定を行う

CORS設定の方法はオフィシャルのドキュメント「クロスオリジン リソース シェアリング(CORS)の設定」に詳しく紹介されています。
この記事ではこのドキュメントに従って以下の作業を行いました。

Cloud SDKのインストール

CORSの設定にCloud SDKのgsutilコンポーネントを利用します。Cloud SDKのインストール方法は、Google Cloud SDK ドキュメントに詳しく説明されています。

CORSの設定

corsの設定ファイルを作成します。
originに接続元のドメインを列挙します。この例では1番上がHostingにデプロイした静的サイトで、次の2つはローカルからの接続になります。

cors-json-file.json
[
  {
    "origin": [
      "https://project********.firebaseapp.com",
      "http://localhost:8080",
      "http://localhost:5000"
    ],
    "responseHeader": ["Content-Type"],
    "method": ["GET"],
    "maxAgeSeconds": 3600
  }
]

CORSの設定にStorageのURLが必要になるのでFirebase ConsoleのStorageで確認します。

sd.png

gsutilコマンドでstorageにcorsの設定を行います。

> gsutil cors set cors-json-file.json <storageのURL>

storageのcors設定を確認します。

> gsutil cors get <storageのURL>

以上でStorageのCORS設定は完了です。

Cloud Functionsで署名付きURLをレスポンスする処理を実装する

HTTP関数の実装

関数名は「filedownload」としました。
署名付きURLを取得するメソッドはFile.getSignedUrlです。
リファレンスのドキュメントに書かれているように署名付きURLには有効期間を設定することが可能です。

Get a signed URL to allow limited time access to the file.

この例ではconfigで2018/05/30まで有効な署名付きURLを生成しています。

const config = {
  action: 'read',
  expires: '2018-05-30'
};

ソースコード全体

functions/index.js
'use strict';
const functions = require('firebase-functions');
const storage = require('@google-cloud/storage')();

exports.filedownload = functions.https.onRequest((req, res) => {
  if (req.method !== 'GET') {
    console.warn('Not Allowed: ' + req.method);
    res.status(405).send('Method Not Allowed');
    return;
  }
  if (!req.query.file) {
    res.status(400).send('Request Parameter');
    return;
  }

  const bucket = storage.bucket(functions.config().fileupload.bucket.name);
  const file = bucket.file(`upload_images/${req.query.file}`);
  file.exists()
    .then(data => {
      console.log(data);
      return new Promise((resolve, reject) => {
        if (!data[0]) {
          reject(new Error(`file not found: ${req.query.file}`));
        } else {
          const config = {
            action: 'read',
            expires: '2018-05-30'
          };
          resolve(file.getSignedUrl(config));
        }
      });
    })
    .then(results => {
      return res.status(200).json({ url: results[0] });
    })
    .catch(err => {
      console.error(err);
      return res.status(400).json({ message: "file not found" });
    });
});

デプロイ

> firebase deploy --only functions:filedownload

きちんとデプロイできているかFirebase ConsoleのFunctionsダッシュボードで確認できます。

動作確認

事前に動作検証用のファイルをStorageへアップロードしておきます。
curlでダウンロードURLにアクセスして署名付きURLを取得します。

> curl "https://us-central1-project********.cloudfunctions.net/filedownload?file=logo.png"
"{ url: \"https://storage.googleapis.com/project*********.appspot.com/upload_images%2Flogo.png?GoogleAccessId=*********@appspot.gserviceaccount.com&Expires=1527638400&Signature=*********\" }"

取得した署名付きURLにアクセスしてみます。

> curl -o logo.png "https://storage.googleapis.com/project*********.appspot.com/upload_images%2Flogo.png?GoogleAccessId=*********@appspot.gserviceaccount.com&Expires=1527638400&Signature=*********"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 79543  100 79543    0     0  99928      0 --:--:-- --:--:-- --:--:--   99k

Vue.jsでファイルをダウンロードするフォームを実装する

ファイルダウンロードページの追加

components/FileDownload.vue
<template>
  <div class="download">
    <h1>File Download</h1>
    <div>
      <form>
        <input type="text" @change="onFileSelected">
        <button @click.prevent="onDownload">Download</button>
      </form>
      <div class="images">
        <p>image list</p>
        <p v-for="(img, index) in imageList" v-bind:key="index">
          <img v-bind:src="img">
        </p>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios'

const instance = axios.create({
  baseURL: process.env.SITE_URL,
  timeout: 10000
})

export default {
  name: 'FileDownloadForm',
  data () {
    return {
      selectedFile: null,
      imageList: []
    }
  },
  methods: {
    onFileSelected (event) {
      this.selectedFile = event.target.value
    },
    onDownload () {
      instance.get(`/download?file=${this.selectedFile}`)
        .then(res => {
          return new Promise((resolve, reject) => {
            if (res.status === 200 && res.data.url) {
              resolve(res.data.url)
            } else {
              reject(new Error('SignedUrl not found'))
            }
          })
        })
        .then(res => {
          this.imageList.push(res)
        })
        .catch(err => {
          console.error(err)
        })
    }
  }
}
</script>

ルーターにルートを追加

ファイルダウンロードページへ遷移できるようにルートを追加します。

router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import FileUploadForm from '@/components/FileUploadForm'
import FileDownloadForm from '@/components/FileDownloadForm'  // 追加

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/form',
      name: 'FileUploadForm',
      component: FileUploadForm
    },
    // ↓ 追加
    {
      path: '/download-form',
      name: 'FileDownloadForm',
      component: FileDownloadForm
    }
    // ↑ 追加
  ]
})

HostingのリライトとCORSの設定

firebase.json
{

  // ...省略...

  "hosting": {

    // ...省略...

    "headers": [
      {
        "source": "**/upload",
        "headers": [
          {
            "key": "Access-Control-Allow-Origin",
            "value": "*"
          }
        ]
      },
      // ↓ 追加
      {
        "source": "**/download",
        "headers": [
          {
            "key": "Access-Control-Allow-Origin",
            "value": "*"
          }
        ]
      }
      // ↑ 追加
    ],
    "rewrites": [
      {
        "source": "/upload",
        "function": "fileupload"
      },
      // ↓ 追加
      {
        "source": "/download",
        "function": "filedownload"
      },
      // ↑ 追加
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

HostingにVue.jsアプリケーションをデプロイ

デプロイ

> npm run build
> firebase deploy --only hosting

or

> firebase deploy

動作確認

コンソールに出力されているHosting URLのアドレスにアクセスして動作確認します。


操作している様子をgifアニメーションにしました。

xee.gif