FlutterからCloud Function経由でFirebaseに動画(または画像/音声)をアップロードする

Firebase に支えられた Flutter アプリケーションを構築する際、ファイル(ビデオ、オーディオ、画像)のアップロードは一般的な機能です。デフォルトでは、多くの場合 Flutter から直接 Firebase Storage を使用します。しかし、場合によってはセキュリティ上、サーバー側処理、または高度なビジネスロジックのために Cloud Function を経由する方が望ましく、あるいは必須となります。以下は Firebase バックエンドを介したバイナリアップロードの完全な実装例です。
クライアント側で Firebase Storage を使用しない理由
Firebase は Flutter アプリケーションから直接ファイルをアップロードするための便利な SDK を提供しています。しかし、このアプローチにはいくつかの制約があります:
- アクセスのロールと権限を細かく管理する必要がある。
- ビジネスロジック(バリデーション、AI 処理など)をカプセル化できない。
- ファイルはそのまま送信され、サーバー側で介入する余地がない。
ユーザーの検証、ビデオの文字起こし、サムネイルの生成など、より細かい制御を行いたい場合は、Cloud Function を経由することが最良の選択になります。
Flutter 側 : Dio を使ったファイルのアップロード
この例では、ファイルはユーザーによって選択され(file_picker、image_picker、またはその他を使用)、multipart/form-data 形式で Firebase の URL(HTTP 経由で公開された Cloud Function)に送信されます。
videoFile は XFile 型です。
以下は該当する Flutter のコードです:
final bytes = await videoFile.readAsBytes();
final formData = FormData.fromMap({
'video': MultipartFile.fromBytes(
bytes,
filename: videoFile.name,
contentType: DioMediaType.parse(videoFile.mimeType ?? 'video/mp4'),
),
});
final response = await dio.post(
_url,
data: formData,
queryParameters: {
'serieId': serieId,
'language': language,
},
options: Options(
headers: {
'Content-Type': 'multipart/form-data',
'x-user-id': userId,
},
),
);
ここでは Dio を使ってファイルを含む multipart の POST リクエストを送信しています。userId はサーバー側で検証されるためにヘッダーで渡されています。
Firebase 側 : multipart/form-data を Busboy で解析する
Firebase はネイティブで multipart ボディのパースをサポートしていません。Flutter から送信されたファイルを取得するために、Node.js ライブラリの busboy を使用します。これはフォームフィールドやファイルを、ストリーミングモードでも抽出することを可能にします。
busboy をインストールします:
npm install busboy
import busboy from "busboy";
import { buffer } from "stream/consumers";
import { Request } from "firebase-functions/v2/https";
import { Response } from "firebase-functions/v1";
import { Readable } from "stream";
interface FilePart {
fieldName: string;
filename: string;
buf: Buffer;
}
interface FormParts {
fields: Record<string, string>;
files: FilePart[];
}
function getParts(req: Request): Promise<FormParts> {
const bb = busboy({ headers: req.headers });
const fields: Record<string, string> = {};
const files: { fieldName: string; filename: string; buf: Promise<Buffer> }[] =
[];
bb.on("field", (fieldName: string, val: string) => (fields[fieldName] = val));
bb.on(
"file",
(fieldName: string, file: Readable, { filename }: { filename: string }) => {
if (filename) {
files.push({ fieldName, filename, buf: buffer(file) });
} else {
file.resume();
}
}
);
return new Promise((resolve) => {
bb.on("finish", () =>
Promise.all(files.map((o) => o.buf)).then((ar) => {
const processedFiles: FilePart[] = files.map((o, i) => ({
fieldName: o.fieldName,
filename: o.filename,
buf: ar[i] || Buffer.alloc(0), // Provide a default empty buffer if undefined
}));
resolve({ fields, files: processedFiles });
})
);
bb.end(req.rawBody);
});
}
export { getParts, addCorsHeaders };
ファイルが抽出され(Buffer 形式)、@google-cloud/storage を介して Firebase Storage に保存できます。
ビジネスロジックとの統合
このユースケースでは、ビデオはシリーズに関連付けられています。したがって、serieId と language のパラメータを URL に渡します。サーバー側では次の点を確認します:
- ユーザーが認証されており、アップロード権限を持っていること、
- リクエストが有効であること、
- そして Firebase(Firestore + Storage)の依存関係を使って
UploadSerieVideoコマンドを呼び出します。
const { files } = await getParts(req);
const result = await command.execute({
serieId,
language,
videoFile: files[0]!.buf,
videoFileName: files[0]!.filename,
});
このアプローチによりビジネスロジックをカプセル化し、データを検証し、権限のあるユーザーだけがこのアップロードを実行できることを保証できます。
結論
Flutter から Cloud Function 経由で Firebase にファイルをアップロードすることは、堅牢で安全、かつ拡張しやすいバックエンドを構築しようとする人にとって強力な戦略です。これにより中間処理を追加したり、リソースへのアクセスを保護したり、ビジネスロジックを一つのバックエンドに集約したりすることが可能になります。
コメント
読み込み中...