FirebaseのStorageが有料化されたので対処法考えてみた
背景:Firebase Storageが使えない!?
Flutterで新しいアプリを作っていたとき、画像を共有する機能を実装しようとFirebase Storageを使おうとしたら、
**"Blazeプランにアップグレードしないと使用できません"**という表示が出てきました。
調べてみると、2024年10月以降、新しくFirebase Storageを使うには、
従量課金制のBlazeプランへの移行が必須になったとのこと。
ちょっとした画像共有のためだけに課金するのは正直避けたい…。
そこで、無料でできる代替手段を模索しました。
解決策:GASとCloudflare Workersで無料の画像アップロードAPIを作成
まず思いついたのが、Google Apps Script(GAS)を使ってGoogle Driveに画像を保存するAPIを作る方法です。
GASはGoogleアカウントさえあれば無料で使え、Google Driveとの連携も簡単なので理想的に見えました。
問題発生:Flutterから直接GASにPOSTすると302エラーが…
Flutterから直接GASのWebアプリURLにリクエストを送ると、302リダイレクトエラーが発生し、
正常に処理が通りませんでした。
原因はGAS側のリダイレクト挙動とFlutterのHTTPクライアントの仕様によるもので、
直接の通信は安定しないことがわかりました。
対処法:Cloudflare Workersを間に挟むことで解決!
そこで導入したのが Cloudflare Workers です。
FlutterからはCloudflare Workerにリクエストを送り、
WorkerからGASに再送することで、302リダイレクトやCORSの問題を回避。
結果として、Flutterから安定して画像をアップロードできる構成が完成しました。
使用したコード(アップロード部分)
GAS側(Google Apps Script)
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const action = data.action;
const apiKey = data.apiKey;
if (apiKey !== YOUR_SECRET_API_KEY) throw new Error('認証失敗');
if (action === "upload") return handleUpload(data);
else throw new Error('不明なアクション');
} catch (err) {
return ContentService.createTextOutput(JSON.stringify({ error: err.message }))
.setMimeType(ContentService.MimeType.JSON);
}
}
function handleUpload(data) {
const bytes = Utilities.base64Decode(data.imageBase64);
const blob = Utilities.newBlob(bytes, 'image/png', data.fileName);
const folder = DriveApp.getFolderById(YOUR_FOLDER_ID);
const file = folder.createFile(blob);
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
return ContentService.createTextOutput(JSON.stringify({
url: `https://drive.google.com/uc?export=view&id=${file.getId()}`,
fileId: file.getId()
})).setMimeType(ContentService.MimeType.JSON);
}
Cloudflare Workers側(Hono使用)
app.post('/upload', async (c) => {
const { imageBase64, fileName, apiKey} = await c.req.json();
const gasResponse = await fetch(c.env.GAS_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'upload',
imageBase64,
fileName,
apiKey: c.env.SECRET_API_KEY,
})
});
const gasResult = await gasResponse.json();
const { url, fileId } = gasResult;
return c.json({
message: 'Upload successful',
fileId,
});
});
Flutter側(画像アップロード処理)
Future<UploadResult> uploadRecipeWithImage({
required XFile image,
}) async {
// 画像をBase64に変換
final bytes = await image.readAsBytes();
final base64Image = base64Encode(bytes);
final fileName = image.name;
// APIエンドポイント(Cloudflare Worker)
final url = Uri.parse('${dotenv.env['API_URL']}/upload');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'imageBase64': base64Image,
'fileName': fileName,
'apiKey': '${dotenv.env['API_KEY']}',
}),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
return UploadResult(
fileId: json["fileId"],
);
} else {
throw Exception('アップロード失敗: ${response.statusCode}');
}
}
Flutterから画像をアップロードする際は、画像をBase64エンコードし、Cloudflare Workerのエンドポイントに対してHTTP POSTリクエストを送る構成になっています。
おわりに
Firebase Storageの無料枠が実質使えなくなった今、
GAS × Cloudflare Workersという構成で、
無料・安定・軽量な画像アップロードAPIを実現できました。
同じような悩みを持つ方の参考になれば幸いです!
Discussion