firebase_storageでPermission denied対応
firebase_storageの扱い方で数時間を溶かしたので、同じような人が出ない為にまとめました。
TL; DL
よくない例
FirebaseStorage _storage = FirebaseStorage(app: myapp); final ref = _storage.ref().child(dirname).child(filename);
よい例1
FirebaseStorage(app: myapp); final ref = FirebaseStorage.instance.ref().child(dirname).child(filename);
よい例2
FirebaseStorage(app: myapp); final _storage = FirebaseStorage.instance; final ref = _storage.ref().child(dirname).child(filename);
firebase_storageを使う
FlutterはFirebaseと親和性が高く、Firebaseのあれこれが簡単にできます。
FirebaseStorageへのアクセスも公式からパッケージが準備されているので、それをpubspec
に記載して、コードを書くだけ。
(build.gradle
を修正する必要はありません)
かんたん!!easy!!
と思っているとハマりました。
firebase_storageのインストール
pubでfirebase_storage
と検索をかけると出てきます。
Usage To use this plugin, add firebase_storage as a dependency in your pubspec.yaml file.
簡潔すぎる気もしますが、firebase_storage
を使うための準備は実際これだけ。
dependencies: flutter: sdk: flutter firebase_core: firebase_auth: firebase_storage: # <= added!
Firebaseを使うためにはfirebase_core
が必要なのと、Storageへのアクセス権限を限定したい場合はfirebase_auth
も必要です。
追記したらflutter packages get
。
ConsoleでStorageの権限を確認
Firebase Console「開発」メニュー内にある「Storage」を選択、「ルール」タブを表示します。
デフォルトはこんな感じ。
service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow read, write: if request.auth != null; } } }
認証済みだったら誰でも読み書きできるよ、という設定。
疎通確認が目的なのでこのまま進めます。
Permission deniedにぶつかる
とりあえずこんな感じでコードを書いてみます。
import 'dart:io' show File; import 'package:intl/intl.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_storage/firebase_storage.dart'; class StorageApi { // myappはFirebaseAppのインスタンス FirebaseStorage _storage = FirebaseStorage(app: myapp); Future<String> uploadImage(File file) async { final timestamp = DateFormat('yyyyMMddHHmmss').format(DateTime.now()); final ref = _storage.ref().child('images').child(timestamp); final uploadTask = ref.putFile(file, StorageMetadata(contentType: 'image/jpeg')); final snapshot = await uploadTask.onComplete; if (snapshot.error == null) { return await snapshot.ref.getDownloadURL(); } else { print(snapshot.error); return null; } } }
FirebaseにログインしたユーザでuploadImageを叩いてみると、Permission denied
。
こんな感じのエラーが続きます。
E/StorageUtil(25832): error getting token java.util.concurrent.ExecutionException: com.google.firebase.internal.api.FirebaseNoSignedInUserException: Please sign in before trying to get a token. W/NetworkRequest(25832): no auth token for request E/StorageException(25832): StorageException has occurred. E/StorageException(25832): User does not have permission to access this object. E/StorageException(25832): Code: -13021 HttpResult: 403 E/StorageException(25832): The server has terminated the upload session
ログインはしてるはず、と思ってcurrentUserをprint。
final currentUser = await FirebaseAuth.instance.currentUser(); print(currentUser);
返ってきます。ログイン済。
他に原因があるのかと思って、Storageのルールを全許可にすると、アップロードできます。
ということは、エラーメッセージ通り認証がうまくいっていないということ。
………ん?🤔
対処法
調べても出てこない!
同様に悩んでいる人は多いのですが、ルールを全許可にすればよいよ、という回答があるばかり。
違う、やりたいことはそうじゃない。
調べ続けて出てきたのがこのIssue。
In fact looking into some of the related issues of the one you mentioned I managed to find a workaround which is simply access the storage with
FirebaseStorage.instance
また、このIssueも。
Not Works
final FirebaseStorage storage = FirebaseStorage(app: app, storageBucket: '...');
Works
final FirebaseStorage storage = FirebaseStorage.instance;
つまり、こういうことか…!
class StorageApi { // FirebaseStorageはここではinitializeするだけ StorageApi() { FirebaseStorage(app: myapp); } Future<String> uploadImage(File file) async { final timestamp = DateFormat('yyyyMMddHHmmss').format(DateTime.now()); // `FirebaseStorage.instance`にアクセスする // 変数に保持したい場合は`FirebaseStorage.instance`を入れる final ref = FirebaseStorage.instance.ref().child('images').child(timestamp); final uploadTask = ref.putFile(file, StorageMetadata(contentType: 'image/jpeg')); final snapshot = await uploadTask.onComplete; if (snapshot.error == null) { return await snapshot.ref.getDownloadURL(); } else { print(snapshot.error); return null; } } }
どういうこと?
FirebaseStorage
はシングルトンなので、一度initializeすると常に同じオブジェクトが返ってきます。
class FirebaseStorage { FirebaseStorage({this.app, this.storageBucket}) { if (_initialized) return; channel.setMethodCallHandler((MethodCall call) async { _methodStreamController.add(call); }); _initialized = true; } // ...(略) static bool _initialized = false; static FirebaseStorage _instance = FirebaseStorage(); static FirebaseStorage get instance => _instance; // ...(略) }
が、FirebaseStorage(app: myapp)
で返ってくるインスタンスとFirebaseStorage.instance
(FirebaseStorage()
)で返ってくるインスタンスは違っていて、使うべきは後者だった、ということでした。
FirebaseAuth
も.instance
でアクセスするのが布石だったな…という反省。
お疲れ様でした。