概要
以前に投稿した「[FirebaseのHostingとCloud Functionsを利用してStorageへファイルをアップロードするデモアプリケーション] (https://qiita.com/rubytomato@github/items/11c7f3fcaf60f5ce3365)」という記事で開発したデモアプリケーションにファイルダウンロード機能を追加しました。
ファイルダウンロードの方法は、まずダウンロードするファイル名をgetリクエストのパラメータに指定し、Storageにファイルが存在すればダウンロード用の署名付きURLをレスポンスします。
次に署名付きURLにアクセスしてStorageから直接ファイルをダウンロードします。
今回実装した機能
- Cloud FunctionsのHTTP関数でgetリクエストされたファイルの署名付きURLをレスポンスする処理を実装します。
- Vue.jsアプリケーションでファイルをダウンロードするフォームを実装します。
- ダウンロードURLをHostingのrewrite機能でHTTP関数のエンドポイントへディスパッチします。
- 署名付きURLを使ってStorageから直接ファイルをダウンロードします。
前回実装した機能
- Cloud FunctionsのHTTP関数でpostリクエストされたファイルをStorageに保存する処理を実装します。
- Vue.jsアプリケーションでファイルをアップロードするフォームを実装します。
- アップロードURLをHostingのrewrite機能でHTTP関数のエンドポイントへディスパッチします。
構成図
アプリケーション開発の事前準備
以前に投稿した「[FirebaseのHostingとCloud Functionsを利用してStorageへファイルをアップロードするデモアプリケーション] (https://qiita.com/rubytomato@github/items/11c7f3fcaf60f5ce3365)」と重複する部分が多いので、ファイルダウンロード機能の追加で必要になった作業のみ記載します。
署名付きURLを発行するための設定
署名付きURLの発行に必要な権限はiam.serviceAccounts.signBlob
です。この権限を含むロール(役割)を新しく作成するか、この権限を持っている既存のロール(サービスアカウントトークン作成者)を利用してサービスアカウントに割り当てます。
この記事では既存のロール(サービスアカウント トークン作成者)を利用する方法にしました。
また、"Identity and Access Management (IAM) API"を有効にする必要があります。
これらの作業は現時点(2018/05)でFirebase Consoleからは行えないので、GCP Consoleから行います。
GCP Console
[GCP Console] (https://console.cloud.google.com/?hl=ja)にログインし、Firebaseで作成したプロジェクトを選択します。
役割(ロール)の設定
GCP Consoleの左側メニューより"IAMと管理" → "IAM"を選択するとメンバーが表示されます。
その中で名前が”App Engine default service account”というメンバーの鉛筆アイコンをクリックします。
「別の役割を追加」をクリックし”サービスアカウントトークン作成者”という役割を追加します。
Identity and Access Management (IAM) APIの有効化
左側メニューの"APIとサービス" → "ライブラリ"を選択し、検索フィールドに”Identity and Access Management”と入力します。検索結果にAPIが表示されるのでクリックします。
「有効にする」ボタンをクリックしてAPIを有効にします。
これで署名付きURLを発行する準備は整いました。
StorageにCORSの設定を行う
CORS設定の方法はオフィシャルのドキュメント「[クロスオリジン リソース シェアリング(CORS)の設定] (https://cloud.google.com/storage/docs/configuring-cors?hl=ja)」に詳しく紹介されています。
この記事ではこのドキュメントに従って以下の作業を行いました。
Cloud SDKのインストール
CORSの設定にCloud SDKのgsutil
コンポーネントを利用します。Cloud SDKのインストール方法は、[Google Cloud SDK ドキュメント] (https://cloud.google.com/sdk/docs/?hl=ja)に詳しく説明されています。
CORSの設定
corsの設定ファイルを作成します。
originに接続元のドメインを列挙します。この例では1番上がHostingにデプロイした静的サイトで、次の2つはローカルからの接続になります。
[
{
"origin": [
"https://project********.firebaseapp.com",
"http://localhost:8080",
"http://localhost:5000"
],
"responseHeader": ["Content-Type"],
"method": ["GET"],
"maxAgeSeconds": 3600
}
]
CORSの設定にStorageのURLが必要になるのでFirebase ConsoleのStorageで確認します。
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] (https://cloud.google.com/nodejs/docs/reference/storage/1.6.x/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'
};
ソースコード全体
'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でファイルをダウンロードするフォームを実装する
ファイルダウンロードページの追加
<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>
ルーターにルートを追加
ファイルダウンロードページへ遷移できるようにルートを追加します。
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の設定
{
// ...省略...
"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アニメーションにしました。