masalibの日記

システム開発、運用と猫の写真ブログです

firebase functionsでサムネイル画像作成

masalib.hatenablog.com

前回でファイルアップができるようになった
その画像をもとに色々できるようなのでまずはサムネイル画像作成をやってみました

やりたい事

firebaseのstorageにファイルがアップされたら
changeイベントを取得してfirebaseのfunctionsを起動して
サムネイル画像を作成する

パッケージの設定

npmのパッケージの設定

/functions/package.json

{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "dependencies": {
    "firebase-admin": "^4.2.1",
    "firebase-functions": "^0.5.7",
    "request": "^2.81.0",
    "lodash": "^4.17.4",
    "request-promise": "^4.2.1",
    "@google-cloud/storage": "^0.4.0",
    "child-process-promise": "^2.2.0",
    "mkdirp": "^0.5.1",
    "mkdirp-promise": "^4.0.0"
  },
  "private": true
}

記載が完了したら

npm installのコマンドでインストールする

/functions/index.js

const mkdirp = require('mkdirp-promise');
// Include a Service Account Key to use a Signed URL
//const gcs = require('@google-cloud/storage')({keyFilename: 'service-account-credentials.json'});
var gcs = require('@google-cloud/storage')({
  projectId: 'angular-study-chat',
  keyFilename: './service-account-credentials.json'
});

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

// Max height and width of the thumbnail in pixels.
const THUMB_MAX_HEIGHT = 200;
const THUMB_MAX_WIDTH = 200;
// Thumbnail prefix added to file names.
const THUMB_PREFIX = 'thumb_';
//thumbnail

exports.generateThumbnail = functions.storage.object().onChange(event => {
  // File and directory paths.
  const filePath = event.data.name;
  const fileDir = path.dirname(filePath);
  const fileName = path.basename(filePath);
  const thumbFilePath = path.normalize(path.join(fileDir, `${THUMB_PREFIX}${fileName}`));
  const tempLocalFile = path.join(os.tmpdir(), filePath);
  const tempLocalDir = path.dirname(tempLocalFile);
  const tempLocalThumbFile = path.join(os.tmpdir(), thumbFilePath);

//  console.log('filePath');
//  console.log(filePath);

  // Exit if this is triggered on a file that is not an image.
  if (!event.data.contentType.startsWith('image/')) {
    console.log('This is not an image.');
    return;
  }

  // Exit if the image is already a thumbnail.
  if (fileName.startsWith(THUMB_PREFIX)) {
    console.log('Already a Thumbnail.');
    return;
  }

  // Exit if this is a move or deletion event.
  if (event.data.resourceState === 'not_exists') {
    console.log('This is a deletion event.');
    return;
  }

  // Cloud Storage files.
  const bucket = gcs.bucket(event.data.bucket);
  const file = bucket.file(filePath);
  const thumbFile = bucket.file(thumbFilePath);

  // Create the temp directory where the storage file will be downloaded.
  return mkdirp(tempLocalDir).then(() => {
    // Download file from bucket.
    return file.download({destination: tempLocalFile});
  }).then(() => {
    console.log('The file has been downloaded to', tempLocalFile);
    // Generate a thumbnail using ImageMagick.
    return spawn('convert', [tempLocalFile, '-thumbnail', `${THUMB_MAX_WIDTH}x${THUMB_MAX_HEIGHT}>`, tempLocalThumbFile]);
  }).then(() => {
    console.log('Thumbnail created at', tempLocalThumbFile);
    // Uploading the Thumbnail.
    return bucket.upload(tempLocalThumbFile, {destination: thumbFilePath});
  }).then(() => {
    console.log('Thumbnail uploaded to Storage at', thumbFilePath);
    // Once the image has been uploaded delete the local files to free up disk space.
    fs.unlinkSync(tempLocalFile);
    fs.unlinkSync(tempLocalThumbFile);
    // Get the Signed URLs for the thumbnail and original image.
    const config = {
      action: 'read',
      expires: '03-01-2500'
    };
    return Promise.all([
      thumbFile.getSignedUrl(config),
      file.getSignedUrl(config)
    ]);
  }).then(results => {
    console.log('Got Signed URLs.');
    const thumbResult = results[0];
    const originalResult = results[1];
    const thumbFileUrl = thumbResult[0];
    const fileUrl = originalResult[0];
    // Add the URLs to the Database
    return admin.database().ref('images').push({path: fileUrl, thumbnail: thumbFileUrl});
  }).then(() => console.log('Thumbnail URLs saved to database.'));
});

記載が終わったら
firebase deploy --only functions
でデプロイするが

Error: ENOENT: no such file or directory, open 'service-account-credentials.json'
    at Error (native)

あれ・・・
サンプルに書いてありました

f:id:masalib:20170903205336j:plain

なのでservice-account-credentials.jsonをつくることにしました
firebaseのコンソール画面で

f:id:masalib:20170903214421j:plain

①歯車のマークをクリックする
②ユーザーと権限をクリックする

f:id:masalib:20170903214451j:plain

①サービスアカウント
②サービスアカウントを作成をクリックする

f:id:masalib:20170903214506j:plain

①サービスアカウント名を適当にいれる
②新しい秘密鍵の提供をクリック
JSONをクリックする
④G Suiteドメイン全体の委任を有効にするをクリック
⑤役割を選択する
⑥ストレージのストレージ管理者

この作成したjsonファイルをservice-account-credentials.jsonにリネームして
/functions/の配下においてもう1度デプロイする
firebase deploy --only functions

画面

f:id:masalib:20170903220107j:plain
f:id:masalib:20170903221028j:plain

ちなみにサービスアカウントをミスをすると以下のエラーがでた

ApiError: Forbidden
    at new util.ApiError (/user_code/node_modules/@google-cloud/storage/node_modules/@google-cloud/common/src/util.js:107:10)
    at Object.parseHttpRespMessage (/user_code/node_modules/@google-cloud/storage/node_modules/@google-cloud/common/src/util.js:149:33)
    at Object.handleResp (/user_code/node_modules/@google-cloud/storage/node_modules/@google-cloud/common/src/util.js:124:18)
    at Duplexify.<anonymous> (/user_code/node_modules/@google-cloud/storage/src/file.js:711:21)
    at emitOne (events.js:96:13)
    at Duplexify.emit (events.js:188:7)
    at emitOne (events.js:96:13)
    at DestroyableTransform.emit (events.js:188:7)
    at emitOne (events.js:96:13)
    at Request.emit (events.js:188:7)
    at Request.<anonymous> (/user_code/node_modules/@google-cloud/storage/node_modules/request/request.js:1108:14)
    at emitOne (events.js:101:20)
    at Request.emit (events.js:188:7)
    at IncomingMessage.<anonymous> (/user_code/node_modules/@google-cloud/storage/node_modules/request/request.js:1091:12)
    at IncomingMessage.g (events.js:292:16)
    at emitNone (events.js:91:20)
    at IncomingMessage.emit (events.js:185:7)
    at endReadableNT (_stream_readable.js:974:12)
    at _combinedTickCallback (internal/process/next_tick.js:80:11)
    at process._tickDomainCallback (internal/process/next_tick.js:128:9)

https://github.com/GoogleCloudPlatform/google-cloud-node/issues/1388
このエラーがでたらサービスアカウントにミスがあります