Posted at

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

More than 1 year has passed since last update.


概要

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