Zenn
🔥

FirebaseのStorageが有料化されたので対処法考えてみた

に公開
2

背景: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を実現できました。

同じような悩みを持つ方の参考になれば幸いです!

2

Discussion

ログインするとコメントできます