Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
654240a
wip implement signed URL generation with V2 and V4 support
demolaf Jan 21, 2026
2612e78
Merge branch 'next' of github.com:invertase/dart_firebase_admin into …
demolaf Jan 21, 2026
a1ad60e
feat: add support for `credentials` and `keyFilename` in `StorageOpti…
demolaf Jan 22, 2026
28eca89
feat: implement signed URL generation with V2 and V4 support
demolaf Jan 22, 2026
b3cff8d
test: add unit and integration tests for Bucket and File signed URL g…
demolaf Jan 22, 2026
2212466
chore: cleanup comments
demolaf Jan 22, 2026
77c4bcb
lint fixes
demolaf Jan 22, 2026
8351173
chore: add Firebase emulator config and storage coverage integration
demolaf Jan 22, 2026
631d6f2
test: improve test coverage and error handling for HmacKey and hash s…
demolaf Jan 22, 2026
84b49d6
chore: update .gitignore to exclude service-account-key.json
demolaf Jan 22, 2026
9d3c953
Merge branch 'next' of github.com:invertase/dart_firebase_admin into …
demolaf Jan 22, 2026
b55c584
docs: add googleapis_storage package to documentation index
demolaf Jan 22, 2026
3e2fcc6
feat(storage): add storage example and e2e tests for signed URL funct…
demolaf Jan 22, 2026
1f815d6
test(storage): add E2E tests for Bucket.getSignedUrl with list action
demolaf Jan 22, 2026
2ab91e6
feat(storage): add Storage service integration with emulator support
demolaf Jan 23, 2026
ca55152
feat: add storage service with emulator support
demolaf Jan 23, 2026
eaa92a3
test: remove emulator env vars from prod tests
demolaf Jan 23, 2026
59c3309
refactor(storage): use intl package for UTC date formatting in signer
demolaf Jan 23, 2026
3a45856
Merge branch 'googleapis-storage-sign' of github.com:invertase/dart_f…
demolaf Jan 23, 2026
b9f8964
fix: CI update env variable types to allow nullable values and improv…
demolaf Jan 23, 2026
58d2194
refactor: remove firebase emulator configurations for googleapis_storage
demolaf Jan 26, 2026
ae16a0b
refactor(storage): simplify credential handling and remove Credential…
demolaf Jan 26, 2026
f09d035
refactor(tests): update integration tests to use simplified credentia…
demolaf Jan 26, 2026
6884813
docs(storage): update credential documentation to include fromService…
demolaf Jan 26, 2026
635f7c1
Merge branch 'googleapis-storage-sign' of github.com:invertase/dart_f…
demolaf Jan 26, 2026
22ba3a3
feat: add comprehensive unit tests for File methods
demolaf Jan 26, 2026
f5c7ac6
wip e2e test for File operations
demolaf Jan 27, 2026
7f73aac
feat: add custom HTTP client to manage decompression
demolaf Jan 27, 2026
e91b40a
feat: add HTTP-level tests for `createReadStream` to validate checksu…
demolaf Jan 27, 2026
1b70222
fix: prevent adding errors to closed sink in upload stream completion
demolaf Jan 28, 2026
8a07d26
test(storage): add integration and unit tests for file operations
demolaf Jan 28, 2026
24fa4b7
refactor(storage): invert decompression logic in `StorageHttpClient`
demolaf Jan 28, 2026
4939f72
test(storage): replace fixed delays with file existence checks in int…
demolaf Jan 28, 2026
e660fa2
test(storage): add integration tests for setStorageClass, rotateEncry…
demolaf Jan 28, 2026
cd8532b
Merge branch 'next' of github.com:invertase/dart_firebase_admin into …
demolaf Jan 28, 2026
135e4af
fix lint errors
demolaf Jan 28, 2026
c5f326c
feat(environment): add unified emulator host access for storage, auth…
demolaf Jan 28, 2026
c860cd6
Merge branch 'admin-storage-wrapper' of github.com:invertase/dart_fir…
demolaf Jan 28, 2026
6bf6986
feat(storage): add V2 and V4 signed POST policy generation for file u…
demolaf Jan 28, 2026
a6e34ef
fix: handle custom endpoints for StorageHttpClient compression routing
demolaf Jan 28, 2026
e964063
Merge branch 'next' of github.com:invertase/dart_firebase_admin into …
demolaf Jan 29, 2026
077f2da
chore: cleanup lint errors
demolaf Jan 29, 2026
70dd2cf
test(storage): add integration and unit tests for moveFileAtomic, res…
demolaf Jan 29, 2026
d9223ea
chore: fix lint errors
demolaf Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions packages/dart_firebase_admin/example/lib/storage_example.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:convert';
import 'package:dart_firebase_admin/dart_firebase_admin.dart';
import 'package:dart_firebase_admin/storage.dart';
import 'package:googleapis_storage/googleapis_storage.dart' hide Storage;

Future<void> storageExample(FirebaseApp admin) async {
Expand All @@ -14,21 +13,35 @@ Future<void> storageExample(FirebaseApp admin) async {
Future<void> basicExample(FirebaseApp admin) async {
print('> Basic Storage usage...\n');

final storage = Storage(admin);
try {
final storage = admin.storage();

final bucket = storage.bucket('dart-firebase-admin.firebasestorage.app');
final bucket = storage.bucket('dart-firebase-admin.firebasestorage.app');
print('> Using bucket: ${bucket.id}\n');

final file = bucket.file('foo.txt');
final file = bucket.file('foo.txt');
print('> File: ${file.name}\n');

const fileContent = 'Hello from basicExample() in storage_example.dart';
print('> Uploading file "${file.name}" to Storage...\n');
await file.save(fileContent);
print('> ✓ File uploaded successfully!\n');

await file.delete();
print('> Deleting file "${file.name}"...\n');
await file.delete();
print('> ✓ File deleted successfully!\n');
} catch (e, stackTrace) {
print('> ✗ Error: $e\n');
print('> Stack trace: $stackTrace\n');
}
}

Future<void> signedUrlExample(FirebaseApp admin) async {
print('> Signed URL Storage usage...\n');

String? url;
try {
final storage = Storage(admin);
final storage = admin.storage();

final bucket = storage.bucket('dart-firebase-admin.firebasestorage.app');
print('> Using bucket: ${bucket.id}\n');
Expand Down
1 change: 1 addition & 0 deletions packages/dart_firebase_admin/example/run_with_emulator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export FIRESTORE_EMULATOR_HOST=localhost:8080
export FIREBASE_AUTH_EMULATOR_HOST=localhost:9099
export CLOUD_TASKS_EMULATOR_HOST=localhost:9499
export FIREBASE_STORAGE_EMULATOR_HOST=localhost:9199
export GOOGLE_CLOUD_PROJECT=dart-firebase-admin

# Run the example
Expand Down
10 changes: 8 additions & 2 deletions packages/dart_firebase_admin/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
"ui": {
"enabled": true
},
"singleProjectMode": true
"singleProjectMode": true,
"storage": {
"port": 9199
}
},
"functions": [
{
Expand All @@ -28,5 +31,8 @@
"*.local"
]
}
]
],
"storage": {
"rules": "storage.rules"
}
}
1 change: 1 addition & 0 deletions packages/dart_firebase_admin/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import '../firestore.dart';
import '../functions.dart';
import '../messaging.dart';
import '../security_rules.dart';
import '../storage.dart';

part 'app/app_exception.dart';
part 'app/app_options.dart';
Expand Down
73 changes: 73 additions & 0 deletions packages/dart_firebase_admin/lib/src/app/environment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ abstract class Environment {
/// Format: `host:port` (e.g., `127.0.0.1:9499`)
static const cloudTasksEmulatorHost = 'CLOUD_TASKS_EMULATOR_HOST';

/// Firebase Storage Emulator host address.
///
/// When set, Storage service automatically connects to the emulator instead of production.
/// Format: `host:port` (e.g., `localhost:9199`)
static const firebaseStorageEmulatorHost = 'FIREBASE_STORAGE_EMULATOR_HOST';

/// Checks if the Firestore emulator is enabled via environment variable.
///
/// Returns `true` if [firestoreEmulatorHost] is set in the environment.
Expand Down Expand Up @@ -87,4 +93,71 @@ abstract class Environment {
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
return env[cloudTasksEmulatorHost] != null;
}

/// Checks if the Storage emulator is enabled via environment variable.
///
/// Returns `true` if [firebaseStorageEmulatorHost] is set in the environment.
///
/// Example:
/// ```dart
/// if (Environment.isStorageEmulatorEnabled()) {
/// print('Using Storage emulator');
/// }
/// ```
static bool isStorageEmulatorEnabled() {
final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
return env[firebaseStorageEmulatorHost] != null;
}

/// Gets the Storage emulator host from environment variables.
///
/// Returns the host:port string if set, otherwise null.
///
/// Example:
/// ```dart
/// final host = Environment.getStorageEmulatorHost();
/// if (host != null) {
/// print('Storage emulator at $host');
/// }
/// ```
static String? getStorageEmulatorHost() {
final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
return env[firebaseStorageEmulatorHost];
}

/// Gets the Auth emulator host from environment variables.
///
/// Returns the host:port string if set, otherwise null.
///
/// Example:
/// ```dart
/// final host = Environment.getAuthEmulatorHost();
/// if (host != null) {
/// print('Auth emulator at $host');
/// }
/// ```
static String? getAuthEmulatorHost() {
final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
return env[firebaseAuthEmulatorHost];
}

/// Gets the Cloud Tasks emulator host from environment variables.
///
/// Returns the host:port string if set, otherwise null.
///
/// Example:
/// ```dart
/// final host = Environment.getCloudTasksEmulatorHost();
/// if (host != null) {
/// print('Tasks emulator at $host');
/// }
/// ```
static String? getCloudTasksEmulatorHost() {
final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
return env[cloudTasksEmulatorHost];
}
}
5 changes: 5 additions & 0 deletions packages/dart_firebase_admin/lib/src/app/firebase_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ class FirebaseApp {
/// Returns a cached instance if one exists, otherwise creates a new one.
Functions functions() => Functions.internal(this);

/// Gets the Storage service instance for this app.
///
/// Returns a cached instance if one exists, otherwise creates a new one.
Storage storage() => Storage.internal(this);

/// Closes this app and cleans up all associated resources.
///
/// This method:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ enum FirebaseServiceType {
firestore(name: 'firestore'),
messaging(name: 'messaging'),
securityRules(name: 'security-rules'),
functions(name: 'functions');
functions(name: 'functions'),
storage(name: 'storage');

const FirebaseServiceType({required this.name});

Expand Down
2 changes: 0 additions & 2 deletions packages/dart_firebase_admin/lib/src/auth.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart' as dart_jsonwebtoken;
import 'package:googleapis/identitytoolkit/v1.dart' as auth1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ class AuthHttpClient {
/// When [Environment.firebaseAuthEmulatorHost] is set, routes requests to
/// the local Auth emulator. Otherwise, uses production Auth API.
Uri get _authApiHost {
final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
final emulatorHost = env[Environment.firebaseAuthEmulatorHost];
final emulatorHost = Environment.getAuthEmulatorHost();

if (emulatorHost != null) {
return Uri.http(emulatorHost, 'identitytoolkit.googleapis.com/');
Expand Down
2 changes: 0 additions & 2 deletions packages/dart_firebase_admin/lib/src/functions/functions.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:googleapis/cloudtasks/v2.dart' as tasks2;
import 'package:googleapis_auth/auth_io.dart' as googleapis_auth;
import 'package:googleapis_auth_utils/googleapis_auth_utils.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ class FunctionsHttpClient {
/// Returns the host:port string (e.g., "localhost:9499") if the
/// CLOUD_TASKS_EMULATOR_HOST environment variable is set.
String? get _cloudTasksEmulatorHost {
final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
final host = env[Environment.cloudTasksEmulatorHost];
return (host != null && host.isNotEmpty) ? host : null;
final emulatorHost = Environment.getCloudTasksEmulatorHost();
return (emulatorHost != null && emulatorHost.isNotEmpty)
? emulatorHost
: null;
}

/// Lazy-initialized HTTP client that's cached for reuse.
Expand Down
48 changes: 0 additions & 48 deletions packages/dart_firebase_admin/lib/src/storage.dart

This file was deleted.

62 changes: 62 additions & 0 deletions packages/dart_firebase_admin/lib/src/storage/storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:googleapis_storage/googleapis_storage.dart'
as googleapis_storage;
import 'package:meta/meta.dart';
import '../app.dart';

class Storage implements FirebaseService {
/// Internal constructor
Storage._(this.app) {
String? apiEndpoint;
final isEmulator = Environment.isStorageEmulatorEnabled();

if (isEmulator) {
final emulatorHost = Environment.getStorageEmulatorHost()!;

if (RegExp('https?://').hasMatch(emulatorHost)) {
throw FirebaseAppException(
AppErrorCode.failedPrecondition,
'FIREBASE_STORAGE_EMULATOR_HOST should not contain a protocol (http or https).',
);
}
apiEndpoint = 'http://$emulatorHost';
}

_delegate = googleapis_storage.Storage(
googleapis_storage.StorageOptions(
authClient: isEmulator ? null : app.client,
apiEndpoint: apiEndpoint,
useAuthWithCustomEndpoint: false,
),
);
}

/// Factory constructor that ensures singleton per app.
@internal
factory Storage.internal(FirebaseApp app) {
return app.getOrInitService(FirebaseServiceType.storage.name, Storage._);
}

@override
final FirebaseApp app;

late final googleapis_storage.Storage _delegate;

googleapis_storage.Bucket bucket(String? name) {
final bucketName = name ?? app.options.storageBucket;
if (bucketName == null || bucketName.isEmpty) {
throw FirebaseAppException(
AppErrorCode.failedPrecondition,
'Bucket name not specified or invalid. Specify a valid bucket name via the '
'storageBucket option when initializing the app, or specify the bucket name '
'explicitly when calling the bucket() method.',
);
}

return _delegate.bucket(bucketName);
}

@override
Future<void> delete() async {
await _delegate.terminate();
}
}
2 changes: 1 addition & 1 deletion packages/dart_firebase_admin/lib/storage.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export 'src/storage.dart';
export 'src/storage/storage.dart';
12 changes: 12 additions & 0 deletions packages/dart_firebase_admin/storage.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
rules_version = '2';

// Craft rules based on data in your Firestore database
// allow write: if firestore.get(
// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin;
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if false;
}
}
}
Loading
Loading