俺、サービス売って家買うんだ

Swift, Kotlin, Vue.js, 統計, GCP / このペースで作ってればいつか2-3億で売れるのがポっと出来るんじゃなかろうか

ExpressでGoogle Cloud Storageに画像をアップロードする

f:id:ie-kau:20170222235628j:plain

やりたきこと

  • Expressで稼働しているWebサービスで画像をGoogle Cloud Storage(以下GCS)にアップロードする
  • サブドメインで画像を閲覧できるようにする

各種バージョン

Node 7.x
Express 4.x

事前準備

以下は完了している

  • ドメインは取得
  • Google Cloud DNSの設定

1. image.[ドメイン].comというサブドメインを設定する

Google Cloud DNSでCNAMEレコードに該当サブドメインを追加します。

f:id:ie-kau:20170222235440p:plain

2. ドメインの所有権の認証をする

ドメイン名を持つバケットを作成するには、GCPにログインしている管理者が対象のドメインの所有者であることを確認しておかなければなりません。

ドメインを持つバケットを作成できるユーザー

ドメインに確認済みのオーナーが複数いる場合、これらのオーナーのみが名前にドメインを使用したバケットを作成することができます。ドメインに確認済のオーナーがいない場合、確認済のウェブサイト オーナーが名前にドメインを使用したバケットを作成することができます。少なくとも 1 人の確認済のオーナーがドメインまたはウェブサイトに含まれていないと、名前にドメインを使用したバケットを作成することはできません。ドメイン所有権はウェブサイト所有権よりも制御レベルが高いため、ドメイン名を持つバケットを作成できるユーザーをサイトで厳しく制御する必要がある場合に役立ちます。

たとえば、「example.com」というサイトの管理を担当する IT スタッフ メンバーが 2 人いるとします。必要な確認(以下を参照)を完了すると、「example.com」、「reports.example.com」、「downloads.example.com」など、ドメイン名を持つバケットを作成できるユーザーはその 2 人のみになります。

確認済のウェブサイトまたはドメイン オーナーは、Search Console を使用してウェブサイトまたはドメイン オーナーを追加することができます。Search Console のダッシュボードで、管理するウェブサイトを検索し、[プロパティの管理] > [ユーザーを追加/削除] を選択します。ドメイン所有者を追加するには、[プロパティ所有者の管理] リンクを選択します。ドメイン所有者は、他のドメイン所有者を追加することができます。

※引用元
Domain-Named Bucket Verification  |  Cloud Storage Documentation  |  Google Cloud Platform

これは引用にあるようにSearch Consoleから対応できます。

3. Google Cloud Platform API に利用する鍵の作成

「IAMと管理」の「サービス アカウント」でGCPのAPIを利用するサービスアカウントと鍵を作成します。

f:id:ie-kau:20170222235332p:plain

ここで作成したjsonの鍵はバケットにファイルをアップロードする時に利用します。

4. GCSのバケットを作る

利用したいサブドメインと同じバケット名でバケットを作ります。 ドメインの所有権が確認されてない場合はここでエラーになります。

f:id:ie-kau:20170222235225p:plain

Expressの設定

利用するモジュール

ソースコード

const express = require('express')
const router = express.Router()
// -- その他色々 -- 
const multer  = require('multer')
const storage = require('@google-cloud/storage')
const gcs = storage({
  projectId: '[projectId]',
  keyFilename: '[/path/to/key.json]' // 事前準備3で作成した鍵
})

const bucket = gcs.bucket('image.[ドメイン].jp')
bucket.acl.default.add({
  entity: "allUsers",  // 外部アクセスを可能にする
  role: storage.acl.READER_ROLE
}, err => {})

const imageUploader = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 10 * 1024 * 1024  // ファイルサイズ上限
  }
}).single('image')

router.post('/upload', (req, res, next) => {
  imageUploader(req, res, err => {
    if (err) {
      var error = null
      switch(err.code) {
        case 'LIMIT_FILE_SIZE':
          error = {code: 1111, message: 'ファイルが大きすぎます'}
          break
        default:
          error = err
          break
      }
      next(error)
      return
    }

    const mimetype = req.file.mimetype
    const type = [
      { ext: 'gif', mime: 'image/gif' },
      { ext: 'jpg', mime: 'image/jpeg' },
      { ext: 'png', mime: 'image/png' }
    ].find(ext => {
      return ext.mime === mimetype
    })

    if (!type) {
      next({code: 1111, message: "不正なファイル形式です"})
      return
    }

    const blob = bucket.file(`uploads/${req.file.originalname}`)
    const opts = { 
      metadata: { 
        contentType: mimetype,
        cacheControl: "public, max-age=300"
      } 
    }
    const blobStream = blob.createWriteStream(opts)

    blobStream.on('error', err => {
      next(err)
      return
    })

    blobStream.on('finish', () => {
      const publicUrl = `http://${bucket.name}/${blob.name}`
      res.json({ok: 1, imageUrl: publicUrl})
    })

    blobStream.end(req.file.buffer)
  })
})

コードは端折りましたがこれで image.[ドメイン].jp のバケットの uploadsというフォルダにファイルをアップロードすることができました。

課題

  • GCSでサーブした場合のSSL対応はどうすればいいのか?