Magika + Laravel で正確なファイルタイプ判別のバリデーションをつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、いつもはエックスから新しいテクノロジーの情報を得ているのですがもう1つ私が情報源にしているのがニュースアプリです。

過去の閲覧情報から「これ、どうせ好きだろ?」みたいなニュースを選別してくれるのでとても快適なんですよね。

そして、この間発見したのが、

Magika(まじか)

です。

📝 参考ページ: GoogleがAIの力でファイル形式を正確に識別するツール「Magika」をオープンソースで公開

Magikaはファイルの形式(mime-typeとか)を判別してくれるGoogleのプロジェクトなんですね。

すると、すぐさま思いました。

Laravel の通常バリデーションよりも精度が高いファイル判別ができるじゃん!

と。

そこで❗

今回はMagikaのファイル判別をLaravelで使えるようにしてみることにしました。

何かの参考になりましたら嬉しいです。😄✨

「シマヤのうどんスープ
サイコー!」

開発環境: Laravel 10.x、FastAPI 0.109.2

前提として

残念ながら、Magika2024.2.20 現在のところPythonJavaScriptのみで動くようです。

そのため、今回はインストールが簡単なPythonで実装することにします。

ただし、PHPの中からexec()などを使って直接Pythonを実行すると、権限をゆるめたりしてセキュリティ上にあまり良いとは言えません。

そのため、今回は以下の手順で「Laravel + Magika」を実装することにします。

  1. Laravel にファイル送信
  2. FastAPI で作った URL にそのファイルを送信
  3. Magika でファイルタイプをチェックし結果を返す
  4. 返ってきた情報を元にしてバリデーションの「OK or ダメ」を決定する

つまり、ちょっとした「マイクロサービス化」するわけですね。

では、今回も楽しんでやっていきましょう❗

Magika をインストールする(Python)

では、まずは以下のコマンドでMagikaをインストールして試してみることにしましょう。

pip install magika

インストールが終わったら、適当なファイルを用意して以下のようにコマンド実行してみてください。

magika test.jpg

test.jpg の部分は適宜変更してください。

すると、以下のように結果を出してくれました。

ただし、今回のケースだとデータを取得しにくいので、--jsonオプションをつけてみましょう。

magika test.jpg --json

すると、このように返してくれます。(便利ですね👍)

FastAPIをインストールする

では、次にFastAPI(とウェブサーバー)をインストールしましょう。
以下のコマンドを実行してください。

pip install fastapi
pip install uvicorn

※なお、私の場合は最初パッケージが足りないと言われ、以下もインストールしました。もしエラーが出る場合は試してみてください。

pip install python-multipart

インストールが完了したら、適当なフォルダをつくって中に以下のファイルを作成します。

api.py

from fastapi import FastAPI, File, UploadFile
from magika import Magika

app = FastAPI()

magika = Magika()

@app.get("/test")
async def test():
    return "test";

@app.post("/file_type_check")
async def create_file(file: UploadFile = File(...)):
    contents = await file.read()
    return magika.identify_bytes(contents)

はい❗

これで、FastAPIを実行すると「http://127.0.0.1:8000/file_type_check」へファイル送信するとMagikaで実行した結果を取得することができるようになりました。

Laravel の独自バリデーションをつくる

では、ここからはLaravel側の作業です。
以下のコマンドで独自バリデーションのファイルを作成してください。

php artisan make:rule Magika

すると、ファイルが作成されるので中身を以下のようにします。

app/Rules/Magika.php

<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;

class Magika implements ValidationRule
{
    public function __construct(private array $mime_types) // 許可する mime-type を指定する
    {
    }

    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $fast_api_url = 'http://127.0.0.1:4989/file_type_check'; // ホントは .env -> config 化すべきです!
        $stream = fopen($value->getRealPath(), 'r+');

        // Magika(FastAPI)にファイルを送信
        $response = Http::attach('file', $stream)->post($fast_api_url);
        $mime_type = '';

        if($response->ok()) {

            $response_data = $response->json();
            $mime_type = Arr::get($response_data, 'output.mime_type');

        }

        if (! in_array($mime_type, $this->mime_types, true)) {

            $fail('ファイルタイプ「' . $mime_type . '」はアップロードできません');

        }
    }
}

コントローラーをつくる

では、先ほどの独自バリデーションでチェックできるようコントローラーをつくります。

以下のコマンドを実行してください。

php artisan make:controller FileUploadController

すると、ファイルが作成されるので中身を以下のようにします。

app/Http/Controllers/FileUploadController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Rules\Magika;

class FileUploadController extends Controller
{
    public function create()
    {
        return view('file_upload.create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'file' => [
                'required',
                'file',
                'max:5120', // 5MB
                new Magika([ // ここで Magika バリデーション・ルールを使う
                    'image/jpeg',
                    'image/png',
                    'image/gif',
                ]),
            ]
        ]);

        return '完了!';
    }
}

なお、new Magika()の引数としてセットしている以下3つのmime-typeがバリデーションを「通過できる」ものになっています。

  • image/jpeg
  • image/png
  • image/gif

つまり、ビットマップやwebpはバリデーション・エラーになります。

ビューをつくる

では、先ほどのコントローラー内でセットしたビューをつくっていきましょう。
以下のコマンドを実行してください。

php artisan make:view file_upload.create

すると、ファイルが作成されるので中身を次のようにしてください。

resources/views/file_upload/create.blade.php

<html>
<head>
    <title>Magika ファイルアップロード・バリデーション</title>
    <script src="https://cdn.tailwindcss.com/3.4.1"></script>
</head>
<body>
    <div class="p-5">
        <form action="{{ route('file_upload.store') }}" method="post" enctype="multipart/form-data">
            @csrf
            <div class="mb-3">
                <input type="file" name="file">
            </div>
            @error('file')
                <div class="text-red-500 bg-red-100 p-3 mb-4 rounded">{{ $message }}</div>
            @enderror
            <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">アップロード</button>
        </form>
    </div>
</body>
</html>

ルートをつくる

では、最後にルートです。
以下を追加しておいてください。

routes/web.php

use App\Http\Controllers\FileUploadController;

// 省略

Route::prefix('file_upload')->controller(FileUploadController::class)->group(function(){

    Route::get('create', 'create')->name('file_upload.create');
    Route::post('store', 'store')->name('file_upload.store');

});

これで作業は完了です!
お疲れ様でした。😄✨

テストしてみる

では、実際にテストしてみましょう❗

まずはPythonFastAPIを起動します。

uvicorn api:app --port 4989 --reload

※ 私の環境ではデフォルトの8000ポートが使用されていたのでポート番号を変えています。

すると、以下のようにFastAPIが待機状態になります。

では、次にLaravel側です。
https://******/file_upload/create」へブラウザでアクセスしてみましょう。

フォームが表示されました。
では、まずはバリデーションが許可されていないwebp画像を送信してみましょう。

すると・・・・・・

はい❗
期待通りにバリデーション・エラーになりました。

では、許可されているjpg画像を送信してみましょう。

はい❗
こちらも想定通りでバリデーションを通過することができました。

すべて成功です😄✨

企業様へのご提案

Magikaを利用することにより、より精度の高いファイルタイプ判別が可能になります。

さらにMagikaは高速で動く(プロジェクトの説明文では「ファイルサイズが大きくても高速で動く」と書いてあります)ので、動画などでもある程度は早く処理ができるかと思います。

もしそういった機能をご希望でしたら、いつでもお気軽にご相談ください。

お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はMagikaを独自バリデーション化してみました。

正直言うと、PHPでも直接使えるようになってほしいなという気持ちですが、FastAPIがあればすぐ実装できますし、APIキーみたいなものを作っておけば他の人に勝手に使われることもないので、複数サイトからアクセスできるMagika専用のAPIを作っておいても良いかもしれないですね。

ちなみに、テキストファイルを空にして送信してみたところ、「inode/x-empty」というmime-typeが返ってきたので、もしかするといろんな例外タイプがあるのかもしれません。

ぜひみなさんもMagikaで遊んでみてくださいね。

ではでは〜❗

「マーケティングの『ペルソナ』
を調べてたら、ゲームの方が
出てくるんですが…」

このエントリーをはてなブックマークに追加       follow us in feedly