0
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 1 year has passed since last update.

Laravel・CloudRunで画像サーバーを設置

Last updated at Posted at 2023-02-26

少し前に作っていた画像サーバーについて整理しました。

自前のサイトで画像を簡単に記事で使えるようにするために作りました。

フレームワークにLaravelを使い、GCPのCloudRunにデプロイし、画像自体はSFTPで外部のサーバーに保存します。

フロントエンドはnext.jsを利用しています。

フロントエンドからのリクエストに応じてCloudRun上のLaravelが外部ストレージに画像の保存、および、画像の取得・表示を行います。

画像保存の流れ
  1. 【クライアント(next.js)】記事入力欄で画像ペースト→サーバーに画像を送信
  2. 【画像サーバー(laravel・CloudRun)】外部サストレージに画像を保存
  3. 【画像サーバー(laravel・CloudRun)】レスポンスで画像URL返却
  4. 【クライアント】 記事入力欄に画像URL挿入
画像表示
  1. 【クライアント(next.js)】記事を表示、記事内の画像URLの表示リクエスト
  2. 【画像サーバー(laravel・CloudRun)】外部ストレージから画像取得してレスポンスを返す
  3. 【クライアント】 記事内で画像を表示

画像サーバー

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 にデプロイします。

  1. GCPのコンソール画面でCloud Runの「サービス作成」を選択します
    サービス – Cloud Run – django-nuxt – Google Cloud コンソール 2023-02-26 09-19-28.png

  2. 「ソースリポジトリから・・・」>「CLOUD BUILDの設定」> 画面右側で利用するリポジトリを選択し「次へ」
    68747470733a2f2f71696974612d696d6167652d73746f7265.png

  3. 利用するブランチを選択、Buildで利用するDockerfileを入力し、「保存」
    image.png

  4. その他必要な情報を入力して、「作成」
    Laravelの.envで入力していた情報は環境変数で設定しています。
    (認証情報はシークレットに設定する方がいいと思います)
    image.png

  5. デプロイが開始します、CloudBuildも実行中の状態になります

  • Cloud Run
    image.png
  • Cloud Build
    image.png
  1. ビルドが完了後、以下の画面になります
  • Cloud Run
    image.png

  • Cloud Build
    image.png

Cloud Run の画面に表示されているURLにアクセスするとLaravelの画面が表示されます。
Cloud Run上にLaravelが正常にデプロイされたことが確認できます。
image.png
image.png

フロントエンドからの利用(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サイトの入力欄テキストエリアです。
記事を入力すると左側に即時反映されるようにしています。(マークダウン対応)
screencast 2023-02-26 09-36-30.gif

以下の画像をコピーして入力欄にペーストしてみます。
image.png

画像サーバーに画像をアップロード後、レスポンスで画像URLを受け取り、
テキストエリアに画像タグが挿入され記事に画像が挿入されます。
screencast 2023-02-26 09-50-21.gif

0
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
0
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?