LoginSignup
31
28

More than 5 years have passed since last update.

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

Posted at

概要

以前に投稿した「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


31
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
28