GCSへのファイルアップロードができるアプリをGoogle Cloud上で動かしたいと思っている方に向けて、参考になるよう備忘録を残します。
なお、作ったアプリの画面はこんな感じです。(実際は検索機能やサムネ作成機能などもありますが、今回のタイトルとは関係ないので純粋にGCSにアップロードする部分のみ、抜粋して載せています)
ホーム画面
ファイル選択
送信後↓
構成
構成は以下の通りです。
GCS:ファイルストレージ
Cloud Run:フロントエンドアプリ(Node.js + Express)
※今回のアプリでは、Node.jsのバージョンは20-slim、Expressは 4.19.2です。
こちらの公式のサンプルコードを元にしております。
変更点としては、
・Bootstrapを使って見た目をいい感じにしたかったのでpugではなくejsを使っている
・他にもページを作るためにRouterでルーティング
・App EngineではなくCloud Runにデプロイ※
※サンプルは App Engine 用のコードですが、途中から必要になって追加したライブラリ(graphicsmagickとimagemagick)がApp Engine上では動かなかったため、Dockerファイルを追加しCloud Buildを使ってCloud Runにデプロイするよう変更しました。
実装例
該当箇所のソースは以下の通りです。form.ejsから入力されたファイルを、upload.jsでCloud Storage APIを使ってアップロードします。
app.js
'use strict';
const process = require('process');
const express = require('express');
const app = express();
const ejs = require('ejs');
app.set('view engine', 'ejs');
app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(express.json());
app.use("/", require("./route/uploads.js"));
const PORT = parseInt(process.env.PORT) || 8080;
app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
console.log('Press Ctrl+C to quit.');
});
upload.js
'use strict';
const process = require('process');
const express = require("express");
const router = express.Router();
// [START gae_storage_app]
const {format} = require('util');
const Multer = require('multer');
const {Storage} = require('@google-cloud/storage');
// Instantiate a storage client
const storage = new Storage();
const multer = Multer({
storage: Multer.memoryStorage(),
limits: {
fileSize: 100 * 1024 * 1024, // ファイルサイズを最大100MBに制限
},
});
const bucket = storage.bucket(process.env.GCLOUD_STORAGE_BUCKET);
router.get('/', (req, res) => {
res.render('form.ejs');
});
router.post('/upload', multer.single('file'), uploadFile, replaceEmbeddings);
async function uploadFile(req, res, next) {
if (!req.file) {
res.status(400).send('No file uploaded.');
return;
}
var mimetype = req.file.mimetype
// Create a new blob in the bucket and upload the file data.
const blob = bucket.file(req.file.originalname);
const blobStream = blob.createWriteStream({
resumable: false,
});
blobStream.on('error', err => {
next(err);
});
blobStream.on('finish', () => {
const publicUrl = format(
`https://storage.cloud.google.com/${bucket.name}/${blob.name}`
);
// res.status(200).send(publicUrl);
res.render('form.ejs', { getUrl: publicUrl});
});
blobStream.end(req.file.buffer);
next();
}
module.exports = router; // 外部から読み込むために
form.ejs
<!doctype html>
<html lang="ja" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
</head>
<body class="bg-dark">
<main class="container-lg pt-5">
<div class="bg-body-tertiary p-5 rounded">
<form method="POST" action="/upload" enctype="multipart/form-data">
<label for="formFile" class="form-label">アップロードするファイルを選択してください</label>
<input class="form-control" type="file" name="file" required>
<div class="mt-4">
<button class="btn btn-primary" type="submit">送信</button>
</div>
</form>
</div>
<% if (locals.getUrl) { %>
<div class="pt-5"></div>
<div class="alert alert-info" role="alert">
アップロードが完了しました。
<%-getUrl %>
</div>
<% } %>
</main>
</body>
</html>
Dockerfile
FROM node:20-slim
# 必要なライブラリを追加RUN set -ex; \
apt-get -y update; \
apt-get -y install imagemagick; \
apt-get -y install ghostscript; \
apt-get -y install graphicsmagick; \
rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
CMD [ "npm", "start" ]
package.json
{
"name": "app-name",
"description": "hogehoge",
"scripts": {
"start": "node app.js",
"build": "NODE_ENV=production webpack",
"gcp-build":"npm run build",
"test": "c8 mocha -p -j 2 system-test/*.test.js --exit --timeout=30000"
},
"engines": {
"node": ">=16.0.0"
},
"dependencies": {
"@google-cloud/bigquery": "^7.7.1",
"@google-cloud/storage": "^7.11.2",
"body-parser": "^1.20.2",
"bootstrap": "^5.3.0",
"buffer": "^6.0.3",
"ejs": "^3.1.10",
"express": "^4.19.2",
"fs": "^0.0.1-security",
"gm": "^1.25.0",
"graphicsmagick": "^0.0.1",
"imagemagick": "^0.1.3",
"multer": "^1.4.5-lts.1",
"pdf-image": "^2.0.0",
"pdf-thumbnail": "^1.0.6",
"pdf2pic": "^3.1.1",
"postcss-cli": "^11.0.0",
"process": "^0.11.10",
"timers": "^0.1.1",
"util": "^0.12.5",
"worker-loader": "^3.0.8"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/multer": "^1.4.7",
"@types/proxyquire": "^1.3.28",
"@types/supertest": "^2.0.12",
"@types/uuid": "^9.0.1",
"autoprefixer": "^10.4.19",
"c8": "^8.0.0",
"mocha": "^10.2.0",
"pdfjs-dist": "^4.3.136",
"postcss": "^8.4.38",
"proxyquire": "^2.1.3",
"supertest": "^6.3.3",
"uuid": "^9.0.0"
},
"version": "1.0.0",
"main": "app.js",
"author": "",
"license": "ISC",
"keywords": []
}
ローカル実行&デプロイ
ローカルで実行するには以下のコマンドを使います。
$ gcloud init
$ export GOOGLE_CLOUD_PROJECT=プロジェクト名
$ export GCLOUD_STORAGE_BUCKET=GCSバケット名
$ npm install
$ npm start
デプロイは以下の通り
$ gcloud run deploy
GOOGLE_CLOUD_PROJECTとGCLOUD_STORAGE_BUCKETは環境変数なので、デプロイ後にコンソールから設定します。なお、一発目のデプロイはエラーになりますが、編集から環境変数を追加することができます。
これでデプロイ完了です。
なお、ImageMagickはPDFファイルの最初の一枚目をサムネにして検索画面に出したいという要望のために後から追加した機能で使っているライブラリで、今回の実装例に載せたコードからは除外しているので、App Engine(standerd)でも動くと思います。
Error: Could not execute GraphicsMagick/ImageMagick: gm "convert" "-density" "100x100" "-quality" "75" "-[0]" "-resize" "200x200!" "-compress" "jpeg" "./images/000545717.1.png" this most likely means the gm/convert binaries can't be found
参考(公式サンプル)
https://github.com/GoogleCloudPlatform/nodejs-docs-samples/tree/main/appengine/storage/standard
API ARIMA AutoML Bard BigQuery Bing ChatGPT Cloud Endpoints Cloud Storage DWH EBPM GAS Generative AI Google Apps Script Google Cloud Google Form Google Workspace IT組織 Outlook PaLM PDF Python ReportLab selenium Statsmodels STL VertexAI Vertex Forecast スクラッチ セミナー ソトミル トレンド分析 トレーニング バッチ予測 世界は女性とデジタルが救う 女性活躍 技術 時系列データ分析 業務効率化 機械学習 特徴量エンジニアリング 生成AI 自動化 評価指標 需要予測