
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、いつもはエックスから新しいテクノロジーの情報を得ているのですがもう1つ私が情報源にしているのがニュースアプリです。
過去の閲覧情報から「これ、どうせ好きだろ?」みたいなニュースを選別してくれるのでとても快適なんですよね。
そして、この間発見したのが、
Magika(まじか)
です。
参考ページ: GoogleがAIの力でファイル形式を正確に識別するツール「Magika」をオープンソースで公開
Magika
はファイルの形式(mime-type
とか)を判別してくれるGoogle
のプロジェクトなんですね。
すると、すぐさま思いました。
Laravel の通常バリデーションよりも精度が高いファイル判別ができるじゃん!
と。
そこで
今回はMagika
のファイル判別をLaravel
で使えるようにしてみることにしました。
何かの参考になりましたら嬉しいです。
「シマヤのうどんスープ
サイコー!」
開発環境: Laravel 10.x、FastAPI 0.109.2
目次 [非表示]
前提として
残念ながら、Magika
は 2024.2.20 現在のところPython
とJavaScript
のみで動くようです。
そのため、今回はインストールが簡単なPython
で実装することにします。
ただし、PHP
の中からexec()
などを使って直接Python
を実行すると、権限をゆるめたりしてセキュリティ上にあまり良いとは言えません。
そのため、今回は以下の手順で「Laravel + Magika」を実装することにします。
- Laravel にファイル送信
- FastAPI で作った URL にそのファイルを送信
- Magika でファイルタイプをチェックし結果を返す
- 返ってきた情報を元にしてバリデーションの「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');
});
これで作業は完了です!
お疲れ様でした。
テストしてみる
では、実際にテストしてみましょう
まずはPython
のFastAPI
を起動します。
uvicorn api:app --port 4989 --reload
※ 私の環境ではデフォルトの8000
ポートが使用されていたのでポート番号を変えています。
すると、以下のようにFastAPI
が待機状態になります。
では、次にLaravel
側です。
「https://******/file_upload/create」へブラウザでアクセスしてみましょう。
フォームが表示されました。
では、まずはバリデーションが許可されていないwebp
画像を送信してみましょう。
すると・・・・・・
はい
期待通りにバリデーション・エラーになりました。
では、許可されているjpg
画像を送信してみましょう。
はい
こちらも想定通りでバリデーションを通過することができました。
すべて成功です
企業様へのご提案
Magika
を利用することにより、より精度の高いファイルタイプ判別が可能になります。
さらにMagika
は高速で動く(プロジェクトの説明文では「ファイルサイズが大きくても高速で動く」と書いてあります)ので、動画などでもある程度は早く処理ができるかと思います。
もしそういった機能をご希望でしたら、いつでもお気軽にご相談ください。
お待ちしております。
おわりに
ということで、今回はMagika
を独自バリデーション化してみました。
正直言うと、PHP
でも直接使えるようになってほしいなという気持ちですが、FastAPI
があればすぐ実装できますし、API
キーみたいなものを作っておけば他の人に勝手に使われることもないので、複数サイトからアクセスできるMagika
専用のAPI
を作っておいても良いかもしれないですね。
ちなみに、テキストファイルを空にして送信してみたところ、「inode/x-empty」というmime-type
が返ってきたので、もしかするといろんな例外タイプがあるのかもしれません。
ぜひみなさんもMagika
で遊んでみてくださいね。
ではでは〜
「マーケティングの『ペルソナ』
を調べてたら、ゲームの方が
出てくるんですが…」