Skip to content
48 changes: 4 additions & 44 deletions packages/dart_firebase_admin/lib/src/storage/storage.dart
Original file line number Diff line number Diff line change
@@ -1,36 +1,14 @@
import 'dart:convert';

import 'package:google_cloud_storage/google_cloud_storage.dart' as gcs;
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import '../app.dart';
import '../utils/native_environment.dart';

part 'storage_exception.dart';

/// An [http.BaseClient] that lazily resolves the real client on each request.
///
/// This allows [Storage] to be constructed synchronously even though
/// [FirebaseApp.client] is asynchronous. The underlying client lifecycle is
/// managed externally (by [FirebaseApp]), so [close] is a no-op here.
class _DeferredHttpClient extends http.BaseClient {
_DeferredHttpClient(this._clientFuture);

final Future<http.Client> _clientFuture;

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
return (await _clientFuture).send(request);
}

@override
void close() {
// The underlying client is managed externally; do not close it here.
}
}

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

if (isEmulator) {
Expand All @@ -42,25 +20,9 @@ class Storage implements FirebaseService {
'FIREBASE_STORAGE_EMULATOR_HOST should not contain a protocol (http or https).',
);
}
// The new gcs.Storage adds the http:// scheme automatically when
// useAuthWithCustomEndpoint is false, so pass only host:port.
apiEndpoint = emulatorHost;
setNativeEnvironmentVariable('STORAGE_EMULATOR_HOST', emulatorHost);
}

// For emulator, use the provided httpClient (e.g. a mock in tests) or a
// plain unauthenticated client. For production, wrap the async auth client
// in a _DeferredHttpClient so construction stays synchronous.
final http.Client httpClient = isEmulator
? _DeferredHttpClient(
Future.value(app.options.httpClient ?? http.Client()),
)
: _DeferredHttpClient(app.client);

_delegate = gcs.Storage(
client: httpClient,
apiEndpoint: apiEndpoint,
useAuthWithCustomEndpoint: false,
);
_delegate = gcs.Storage();
}

@internal
Expand All @@ -73,7 +35,7 @@ class Storage implements FirebaseService {

late final gcs.Storage _delegate;

gcs.Bucket bucket(String? name) {
gcs.Bucket bucket([String? name]) {
final bucketName = name ?? app.options.storageBucket;
if (bucketName == null || bucketName.isEmpty) {
throw FirebaseAppException(
Expand Down Expand Up @@ -134,8 +96,6 @@ class Storage implements FirebaseService {

@override
Future<void> delete() async {
// _delegate.close() calls close() on our _DeferredHttpClient, which is a
// no-op, so the externally-managed http client is not closed here.
_delegate.close();
}
}
46 changes: 46 additions & 0 deletions packages/dart_firebase_admin/lib/src/utils/native_environment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'dart:ffi';
import 'dart:io';

import 'package:ffi/ffi.dart';
import 'package:meta/meta.dart';

import '../../dart_firebase_admin.dart';

final int Function(Pointer<Utf8>, Pointer<Utf8>, int) _setenv =
DynamicLibrary.process().lookupFunction<
Int32 Function(Pointer<Utf8>, Pointer<Utf8>, Int32),
int Function(Pointer<Utf8>, Pointer<Utf8>, int)
>('setenv');

final int Function(Pointer<Utf16>, Pointer<Utf16>) _setEnvironmentVariableW =
DynamicLibrary.open('kernel32.dll').lookupFunction<
Int32 Function(Pointer<Utf16>, Pointer<Utf16>),
int Function(Pointer<Utf16>, Pointer<Utf16>)
>('SetEnvironmentVariableW');

@internal
void setNativeEnvironmentVariable(String name, String value) {
if (Platform.isWindows) {
using((arena) {
final namePtr = name.toNativeUtf16(allocator: arena);
final valuePtr = value.toNativeUtf16(allocator: arena);
if (_setEnvironmentVariableW(namePtr, valuePtr) == 0) {
throw FirebaseAppException(
AppErrorCode.internalError,
'Failed to set native environment variable: $name',
);
}
});
} else {
using((arena) {
final namePtr = name.toNativeUtf8(allocator: arena);
final valuePtr = value.toNativeUtf8(allocator: arena);
if (_setenv(namePtr, valuePtr, 1) == -1) {
throw FirebaseAppException(
AppErrorCode.internalError,
'Failed to set native environment variable: $name',
);
}
});
}
}
5 changes: 4 additions & 1 deletion packages/dart_firebase_admin/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ dependencies:
collection: ^1.18.0
dart_jsonwebtoken: ^3.0.0
equatable: ^2.0.7
ffi: ^2.1.2
google_cloud: ^0.3.0
google_cloud_firestore: ^0.1.0
google_cloud_storage: ^0.5.1
# `google_cloud_storage` will not change the surface used by
# `dart_firebase_admin`.
google_cloud_storage: ">=0.5.1"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the version constraint here with the idea that there are only three things that package:google_cloud_storage has to keep stable:

  1. having a Storage constructor that call be called without arguments
  2. Storage inferring the emulator settings from environment variables
  3. Storage.bucket creating a Bucket when called with a single argument

If we use more of the API surface than that, then maybe we need to pin the major version.

googleapis: ^15.0.0
googleapis_auth: ^2.2.0
googleapis_beta: ^9.0.0
Expand Down
Loading