少し前に作っていた画像サーバーについて整理しました。
自前のサイトで画像を簡単に記事で使えるようにするために作りました。
フレームワークにLaravelを使い、GCPのCloudRunにデプロイし、画像自体はSFTPで外部のサーバーに保存します。
フロントエンドはnext.jsを利用しています。
フロントエンドからのリクエストに応じてCloudRun上のLaravelが外部ストレージに画像の保存、および、画像の取得・表示を行います。
画像保存の流れ
- 【クライアント(next.js)】記事入力欄で画像ペースト→サーバーに画像を送信
- 【画像サーバー(laravel・CloudRun)】外部サストレージに画像を保存
- 【画像サーバー(laravel・CloudRun)】レスポンスで画像URL返却
- 【クライアント】 記事入力欄に画像URL挿入
画像表示
- 【クライアント(next.js)】記事を表示、記事内の画像URLの表示リクエスト
- 【画像サーバー(laravel・CloudRun)】外部ストレージから画像取得してレスポンスを返す
- 【クライアント】 記事内で画像を表示
画像サーバー
Dockerを利用しています。
# php -v
PHP 8.1.5 (cli) (built: Apr 18 2022 23:52:55) (NTS)
Laravelインストール
composer create-project --prefer-dist laravel/laravel:^9.0
## php artisan
Laravel Framework 9.17.0
flysystem-sftp のインストールと設定
Laravelでsftpを利用するためにインストールします
composer require league/flysystem-sftp-v3 "^3.0"
config/filesystems.php に接続情報を設定します。
return [
'disks' => [
// sftp用の設定を追記
'sftp' => [
'driver' => 'sftp',
'host' => env('SFTP_HOST'),
'username' => env('SFTP_USERNAME'),
'password' => env('SFTP_PASSWORD'),
'port' => env('SFTP_PORT', 22),
'root' => env('SFTP_ROOT', ''),
'visibility' => 'public',
'permPublic' => 0755,
],
],
.envにsftpの接続情報を記入します。
※後半のCloudRunへのデプロイ時は、GCPのコンソール画面で環境変数として設定します
SFTP_HOST=sftpのホスト
SFTP_USERNAME=sftpのユーザ名
SFTP_PASSWORD=sftpのパスワード
SFTP_ROOT=sftp接続時のルートディレクトリ
画像保存処理
画像保存と画像表示の処理のみを行うサーバーなのでroutes/api.phpに処理を書きました。
■メソッド
POST
■URL
/api/image
Route::post('/image', function (Request $request) {
// base64文字列(data:image/png;base64,iVBORw~~)を受取り
$base64 = $request->input('base64');
// カンマで分割
$base64_ex = explode(',',$base64);
$extension = explode('/',explode(';',$base64_ex[0])[0])[1];
// カンマ以降の文字列
$fileData = base64_decode($base64_ex[1]);
// ファイル名にUUIDを発行
$filename_body = Str::orderedUuid()->toString();
// base64の画像データを一時ファイルに保存
$tmpFilePath = sys_get_temp_dir() . '/' . $filename_body.'.'.$extension;
file_put_contents($tmpFilePath, $fileData);
// sftpで画像保存サーバーにファイルを保存
// Storage::disk('sftp')で、config/filesystems.php に定義したsftpサーバーをストレージとして扱います。
Storage::disk('sftp')->putFileAs( 'public', $tmpFilePath, $filename_body.'.'.$extension);
// アクセス用URLをレスポンスで返却
return response()->json([asset('api/image/'.$filename_body.'.'.$extension)]);
});
保存した画像の表示処理
■メソッド
GET
■URL
/api/image/ファイル名
Route::get('/image/{filename}', function (Request $request, string $filename) {
$filedata = Storage::disk('sftp')->get('public/'.$filename);
$extension = explode('.', $filename)[1];
return response(($filedata))->header('content-Type', 'image/'. $extension);
});
Cloud Run へのデプロイ
Laravel の画像サーバーを GCP の CloudRun にデプロイします。
-
その他必要な情報を入力して、「作成」
Laravelの.envで入力していた情報は環境変数で設定しています。
(認証情報はシークレットに設定する方がいいと思います)
-
デプロイが開始します、CloudBuildも実行中の状態になります
- ビルドが完了後、以下の画面になります
Cloud Run の画面に表示されているURLにアクセスするとLaravelの画面が表示されます。
Cloud Run上にLaravelが正常にデプロイされたことが確認できます。
フロントエンドからの利用(next.js)
テキストエリアで画像ペーストすると画像保存リクエストが行われるように処理を組み込みます。
テキストエリア
<Textarea
placeholder="記事を入力"
value={body}
onChange={(e) => setBody(e.target.value)}
onPaste={(e) => imagePaste(e)}
/>
画像ペースト時の保存リクエスト
function imagePaste(event: React.ClipboardEvent<HTMLTextAreaElement>) {
const _event = event
// event からクリップボードのアイテムを取り出す
const items = event.clipboardData.items; // ここがミソ
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item.type.indexOf("image") != -1) {
const file: any = item.getAsFile();
let canvas_size = 900;
const canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d"),
image = new Image(),
size_max = canvas_size;
canvas.width = canvas.height = 0;
const reader = new FileReader();
reader.onload = (event: ProgressEvent<FileReader>) => {
image.onload = async (event2: Event) => {
let w;
let h;
if (image.width >= image.height) {
w = (image.width >= size_max ? size_max : image.width);
h = image.height * (w / image.width);
} else {
h = (image.height >= size_max ? size_max : image.height);
w = image.width * (h / image.height);
}
canvas.width = w;
canvas.height = h;
(ctx as any).drawImage(event2.target, 0, 0, image.width, image.height, 0, 0, w, h);
const temp = (canvas.toDataURL((file as any).type, 0.5));
const response = await axios.post('https://[CloudRunのURL]/api/image',{
base64:temp
})
const pos = (_event as any).target.selectionStart;
const len = body.length;
// setBody(body.substr(0, pos) + '\n\n![](' + temp + ')\n\n' + body.substr(pos, len))
setBody(body.substr(0, pos) + '\n\n![](' + response.data[0] + ')\n\n' + body.substr(pos, len))
}
image.src = (event as any).target.result;
}
reader.readAsDataURL(file);
break;
}
}
}
画像アップロードの動作
next.jsサイトの入力欄テキストエリアです。
記事を入力すると左側に即時反映されるようにしています。(マークダウン対応)
画像サーバーに画像をアップロード後、レスポンスで画像URLを受け取り、
テキストエリアに画像タグが挿入され記事に画像が挿入されます。