3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

第8回 2020年版 React+Firebaseで画像のアップロード(その3)

Last updated at Posted at 2020-04-01

1. 概要

前回の記事でFirebaseに画像をアップロードするプログラムを開発しましたが、今回はFirebase Cloud Functionsを利用して、画像アップロードをトリガーにサムネイルを作成する方法について説明します。

2. 前提条件

作業日時

  • 2020/3/24

環境

  • MacBook Pro (15-inch, 2018)
  • macOS Catalina 10.15.2(19C57)

ソフトウェアのバージョン

分類 ソフトウェア バージョン
静的型付け TypeScript 3.7.5
Firebase CLI firebase-tools 7.15.1
ライブラリ firebase-admin 8.6.0
ライブラリ firebase-functions 3.3.0

3. 前提条件

前々回の記事で、Firebase SDK for Cloud Functions は初期化されている前提です。 firebase init で初期化をすると、以下のようなファイル群が作成されます。
*functions以外のファイルについては割愛しています。

firebase-storage-sample
 +- .firebaserc    # Hidden file that helps you quickly switch between
 |                 # projects with `firebase use`
 |
 +- firebase.json  # Describes properties for your project
 |
 +- functions/     # Directory containing all your functions code
      |
      +- .eslintrc.json  # Optional file containing rules for JavaScript linting.
      |
      +- package.json  # npm package file describing your Cloud Functions code
      |
      +- index.ts      # main source file for your Cloud Functions code
      |
      +- node_modules/ # directory where your dependencies (declared in
                       # package.json) are installed

4. Node.jsのバージョンをCloud Functionsの対応バージョンに合わせる

Cloud Functionsはv8かv10(ベータ)しか対応していないため、開発環境のNode.jsのバージョンをいずれかに合わせる必要があります。

4.1. Cloud functionsのNode.jsのバージョンの設定

以下funcions配下にあるpackage.jsonenginesの設定で、Node.jsのバージョンを指定します。 今回はv10を利用するため、engines10 を指定します。

functions/package.json
{
  "name": "functions",
  "scripts": {
    "lint": "tslint --project tsconfig.json",
    "build": "tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "10"
  },
  "main": "lib/index.js",
  "dependencies": {
    "firebase-admin": "^8.6.0",
    "firebase-functions": "^3.3.0"
  },
  "devDependencies": {
    "tslint": "^5.12.0",
    "typescript": "^3.2.2",
    "firebase-functions-test": "^0.1.6"
  },
  "private": true
}

4.2. 開発環境のNode.jsのバージョンの設定

次に開発環境のNode.jsのバージョンの設定します。まず、nodebrew ls-remoteで使えるバージョンの一覧を表示します。

$ node -v
v13.11.0
$ nodebrew ls-remote

Cloud Functions の Node.js 10 ランタイムは Node.js バージョン 10.15.3 に基づいているため、開発環境のバージョンも10.15.3に合わせます。

$ nodebrew install v10.15.3
$ nodebrew use v10.15.3
use v10.15.3
$  nodebrew list
v10.15.3
v13.11.0

current: v10.15.3

$ node -v
v10.15.3

5. ライブラリの追加

functionsのディレクトリでライブラリの追加を行う。
child-process-promise

$ cd functions/
$ yarn add child-process-promise

*もしかしたら、yarn add @google-cloud/storage@google-cloud/storageも追加が必要だったかもしれません。

6. 関数の作成

6.1 関数ごとにファイルを分割可能とする

複数のエントリポイントをファイルを分割して開発が可能なように、index.tsから外部のモジュールを読み込めるように修正します。
以下のように修正し、関数を追加する際は/func/配下にファイルを追加します。

/functions/src/index.ts
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp(); // initializeAppは一回だけ実行する

/*
 * 配下にあるプログラムを読み込む。
 * エントリポイントを追加する際は、こちらにも追加する。
 */
const cloud_functions = {
  // Write function references
  addMessage: './func/addMessage',
  makeUppercase: './func/makeUppercase',
  generateThumbnail: './func/generateThumbnail',
};

const loadFunctions = (funcs: any) => {
  for (let name in funcs) {
    if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === name) {
      exports[name] = require(funcs[name]);
    }
  }
};

loadFunctions(cloud_functions);

6.2 各関数の作成

HTTPリクエストのトリガーの関数

サムネイルには関係ありませんが、練習を兼ねて、HTTPリクエストをトリガーとして、Realtime Databaseに書き込むプログラムを作成します。

プログラムの最初に firebase-functions および firebase-admin のモジュールを読み込みます。

HTTPのトリガでは、エンドポイントに対するリクエストを行うと、Express.JS スタイルの Request オブジェクトと Response オブジェクトが onRequest() コールバックに渡されます。このサンプルでは、HTTPリクエストで受けたテキスト値をRealtime Databaseの /messages/:pushId/originalに挿入します。

リージョンはasia-northeast1を選択している。

/functions/src/func/addMessage.ts
import * as functions from 'firebase-functions';
const admin = require('firebase-admin');

const region = 'asia-northeast1';

// Take the text parameter passed to this HTTP endpoint and insert it into the
// Realtime Database under the path /messages/:pushId/original
module.exports = functions.region(region).https.onRequest(async (request, response) => {
  try {
    // Grab the text parameter.
    const original = request.query.text;
    // Push the new message into the Realtime Database using the Firebase Admin SDK.
    const snapshot = await admin.database().ref('/messages').push({ original: original });
    // Redirect with 303 SEE OTHER to the URL of the pushed object in the Firebase console.
    response.redirect(303, snapshot.ref.toString());
  }
  catch (error) {
    console.log(error);
    response.status(500).send(error);
  }
});

Realtime Databaseのトリガーの関数

このサンプルではaddMessageでRealtime Databaseにテキストが追加されたことを検知して、その文字列を大文字に変換します。

/functions/src/func/makeUppercase.ts
import * as functions from 'firebase-functions';

// Realtime Databaseのイベントトリガーを利用する場合の推奨リージョンはus-central1となる。
const region = 'us-central1';

/*
  Realtime Database に書き込まれるときに実行される。
  {} で囲まれたものは、コールバックで利用可能な「パラメータ」となる。
*/
module.exports = functions.region(region).database.ref('/messages/{pushId}/original').onCreate((snapshot, context) => {
    // Grab the current value of what was written to the Realtime Database.
    const original = snapshot.val();
    console.log('Uppercasing', context.params.pushId, original);
    const uppercase = original.toUpperCase();
    // You must return a Promise when performing asynchronous tasks inside a Functions such as
    // writing to the Firebase Realtime Database.
    // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
    if (snapshot.ref.parent !== null) {
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    }
    else {
      return undefined;
    }
  });

Storage トリガーの関数

やっとサムネイルのサンプルですが、Storageへの変更をトリガーとする関数を作成します。Storageにファイルが追加された時に本関数が実行されます。contentTypeで画像のみを処理するようにしています。

/functions/src/func/generateThumbnail.ts
import * as functions from 'firebase-functions';
const admin = require('firebase-admin');

//import * as spawnts from 'child-process-promise';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';

const spawn = require('child-process-promise').spawn;

const region = 'asia-northeast1';
const THUMB_PREFIX = 'thumb_';

/*
 generateThumbnail
 */
module.exports = functions.region(region).storage.object().onFinalize(async (object) => {
    const filePath = object.name; // File path in the bucket. 
    const contentType = object.contentType; // File content type.
    const fileBucket = object.contentType; // File content type.
    const metageneration = object.metageneration; // Number of times metadata has been generated. New objects have a value of 1.
    console.log('File path:' + filePath); // images/thumb_Material-uiサンプル.png
    console.log('fileBucket:' + fileBucket); // react-sample-d086e.appspot.com
    console.log('contentType:' + contentType); // imaga/png
    console.log('metageneration:' + metageneration); // 1
    //
    if (filePath === undefined) {
        console.log('File path is undefined.');
        return null;
    }
    // 画像以外だったら何もしない
    if (contentType === undefined || !contentType.startsWith('image/')) {
        console.log('This is not an image.');
        return null;
    }
    // サムネイル画像であった場合何もしない
    const fileName = path.basename(filePath);
    if (fileName.startsWith(THUMB_PREFIX)) {
        console.log('Already a Thumbnail.');
        return null;
    }

    const fileDir = path.dirname(filePath);
    const thumbFilePath = path.normalize(path.join(fileDir, `${THUMB_PREFIX}${fileName}`));
    //    const extension = "png";
    //    const thumbFilePath2 = path.normalize(path.format({dir: fileDir, name: `${THUMB_PREFIX}${fileName}`, ext: extension}));
    const tempFilePath = path.join(os.tmpdir(), fileName);
    const metadata = { contentType: contentType };
    // 出力先のバケット
    const storage = admin.storage();
    const bucket = storage.bucket(fileBucket);
    const file = bucket.file(filePath);

    (async () => {
        // バケットにアップロードされたファイルを仮想マシンのテンプディレクトリにダウンロード
        await file.download({ destination: tempFilePath });
        console.log('The file has been downloaded to', tempFilePath); // tmp/react_icon.png
        // Generate a thumbnail using ImageMagick.
        await spawn('convert', [tempFilePath, '-thumbnail', '200x200>', tempFilePath]);
        console.log('Thumb image created at', tempFilePath);
        // リサイズされたサムネイルをバケットにアップロード
        await bucket.upload(tempFilePath, { destination: thumbFilePath, metadata: metadata });
        console.log('Thumb image uploaded to Storage at', thumbFilePath);
        // Once the thumbnail has been uploaded delete the local file to free up disk space.
        fs.unlinkSync(tempFilePath);
    })()
        .then(() => { console.log('Generate Thumbnail Success!'); })
        .catch((error) => { console.error(error); });
        
    return null;
});

7. functionのデプロイ

7.1. デプロイ

プロジェクトのルートフォルダで以下コマンドを実行し、functionをデプロイできる。各関数がデプロイされているロケーションがプログラムで指定しているものになっていることが確認できます。

$ firebase deploy --only functions

=== Deploying to 'sample-9f36d'...

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint

> functions@ lint /Users/tayamat/Documents/00_mygit/hoshimado/firebase-storage-sample/functions
> tslint --project tsconfig.json

Running command: npm --prefix "$RESOURCE_DIR" run build

> functions@ build /Users/tayamat/Documents/00_mygit/hoshimado/firebase-storage-sample/functions
> tsc

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (90 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: updating Node.js 10 (Beta) function addMessage(asia-northeast1)...
i  functions: updating Node.js 10 (Beta) function makeUppercase(us-central1)...
i  functions: updating Node.js 10 (Beta) function generateThumbnail(asia-northeast1)...
✔  functions[addMessage(asia-northeast1)]: Successful update operation. 
✔  functions[makeUppercase(us-central1)]: Successful update operation. 
✔  functions[generateThumbnail(asia-northeast1)]: Successful update operation. 

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/sample-9f36d/overview
macbookpro-tt:firebase-storage-sample tayamat$ 

7.2 デプロイの確認

FirebaseのWebコンソールでfunctionsに関数が追加されていることを確認します。

2020-03-29_depoy-functions.png

8. 動作確認

8.1. HTTPリクエスト

テキストクエリ パラメータを addMessage() URL に追加し、ブラウザで開きます。

https://us-central1-MY_PROJECT.cloudfunctions.net/addMessage?text=uppercaseme

関数によりブラウザが実行され、テキスト文字列が格納されているデータベースの場所にある Firebase コンソールにリダイレクトされます。テキスト値がコンソールに表示されます。

Function URL (addMessage): https://us-central1-MY_PROJECT.cloudfunctions.net/addMessage

8.2. 画像の変換

前回作成したプログラムをyarn startでサーバーを起動し、ブラウザで表示します。

ReactDropzoneサンプル.gif

FirebaseのWebコンソールでファイルがアップロードされていること、また、サムネイルが作成されていることを確認します。

2020-03-20_Firebase_storage.png

9. 最後に

今回はFirebase Cloud Functionsを利用して、画像アップロードをトリガーにサムネイルを作成する方法について説明しました。
ですが、実はサムネイルの作成だけであれば、Firebaseの拡張機能で同様のことが実現できます。あくまで学習のためにcloud functionsでサムネイルを作成してみました。

2020-04-02_firebase_extensions.png

10. 関連記事

Reactに関する記事です。

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?