Skip to content

Commit

Permalink
feat: Add support for the Firebase Storage emulator
Browse files Browse the repository at this point in the history
  • Loading branch information
jskeet committed Jan 20, 2024
1 parent 138e567 commit a454073
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
using Google.Apis.Http;
using Google.Apis.Services;
using Google.Apis.Storage.v1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -27,6 +30,9 @@ namespace Google.Cloud.Storage.V1
/// </summary>
public sealed class StorageClientBuilder : ClientBuilderBase<StorageClient>
{
private const string s_emulatorHostVariable = "STORAGE_EMULATOR_HOST";
private static readonly string[] s_emulatorEnvironmentVariables = { s_emulatorHostVariable };

/// <summary>
/// If set to true, no credentials are created when the client is built.
/// </summary>
Expand All @@ -49,6 +55,15 @@ public sealed class StorageClientBuilder : ClientBuilderBase<StorageClient>
/// </summary>
internal IScheduler Scheduler { get; set; }

/// <summary>
/// Specifies how the builder responds to the presence of the STORAGE_EMULATOR_HOST emulator environment variable.
/// </summary>
/// <remarks>
/// This property defaults to <see cref="EmulatorDetection.None"/>, meaning that the environment variable is
/// ignored.
/// </remarks>
public EmulatorDetection EmulatorDetection { get; set; } = EmulatorDetection.None;

/// <summary>
/// Creates a new builder with default settings.
/// </summary>
Expand All @@ -60,6 +75,12 @@ public StorageClientBuilder()
/// <inheritdoc />
public override StorageClient Build()
{
var emulatorBuilder = MaybeUseEmulator();
if (emulatorBuilder is object)
{
var ret = emulatorBuilder.Build();
return ret;
}
Validate();
var initializer = CreateServiceInitializer();
var service = new StorageService(initializer);
Expand All @@ -69,6 +90,12 @@ public override StorageClient Build()
/// <inheritdoc />
public override async Task<StorageClient> BuildAsync(CancellationToken cancellationToken = default)
{
var emulatorBuilder = MaybeUseEmulator();
if (emulatorBuilder is object)
{
var ret = await emulatorBuilder.BuildAsync().ConfigureAwait(false);
return ret;
}
Validate();
var initializer = await CreateServiceInitializerAsync(cancellationToken).ConfigureAwait(false);
var service = new StorageService(initializer);
Expand Down Expand Up @@ -114,5 +141,122 @@ protected override void Validate()
GaxPreconditions.CheckState(!UnauthenticatedAccess || (ApiKey == null && Credential == null && CredentialsPath == null && JsonCredentials == null),
"When requesting unauthenticated access, don't specify any other credentials.");
}

private StorageClientBuilder MaybeUseEmulator()
{
var environment = GetEmulatorEnvironment(s_emulatorEnvironmentVariables, s_emulatorEnvironmentVariables);
if (environment is null)
{
return null;
}
var host = environment[s_emulatorHostVariable];

// Allow the host to be specified as a URI or just a name/port.
if (!host.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
!host.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
host = "http://" + host;
}

var builder = new StorageClientBuilder();
builder.CopySettingsForEmulator(this);
builder.BaseUri = host;
builder.UnauthenticatedAccess = true;
return builder;
}

// Copied and adapted from Google.Api.Gax.Grpc.ClientBuilderBase<T>, so that
// if we want to move the code to the Google.Api.Gax.Rest.ClientBuilderBase<T> it will be minimally disruptive.
private Dictionary<string, string> GetEmulatorEnvironment(
IEnumerable<string> requiredEmulatorEnvironmentVariables,
IEnumerable<string> allEmulatorEnvironmentVariables,
Func<string, string> environmentVariableProvider = null)
{
environmentVariableProvider ??= Environment.GetEnvironmentVariable;
var environment = allEmulatorEnvironmentVariables.ToDictionary(key => key, key => GetEnvironmentVariableOrNull(key));

switch (EmulatorDetection)
{
case EmulatorDetection.None:
return default;
case EmulatorDetection.ProductionOnly:
foreach (var variable in allEmulatorEnvironmentVariables)
{
GaxPreconditions.CheckState(
environment[variable] is null,
"Emulator environment variable '{0}' is set, contrary to use of {1}.{2}",
variable, nameof(EmulatorDetection), nameof(EmulatorDetection.ProductionOnly));
}
return default;
case EmulatorDetection.EmulatorOnly:
foreach (var variable in requiredEmulatorEnvironmentVariables)
{
GaxPreconditions.CheckState(
environment[variable] is object,
"Emulator environment variable '{0}' is not set, contrary to use of {1}.{2}",
variable, nameof(EmulatorDetection), nameof(EmulatorDetection.EmulatorOnly));
}
// When the settings *only* support the use of an emulator, the other properties shouldn't be set.
CheckNotSet(BaseUri, nameof(BaseUri));
CheckNotSet(CredentialsPath, nameof(CredentialsPath));
CheckNotSet(JsonCredentials, nameof(JsonCredentials));
CheckNotSet(QuotaProject, nameof(QuotaProject));
CheckNotSet(Credential, nameof(Credential));
CheckNotSet(ApiKey, nameof(ApiKey));
CheckNotSet(GoogleCredential, nameof(GoogleCredential));

void CheckNotSet(object obj, string name)
{
GaxPreconditions.CheckState(obj is null, "{0} is set, contrary to use of {1}.{2}",
name, nameof(EmulatorDetection), nameof(EmulatorDetection.EmulatorOnly));
}
return environment;
case EmulatorDetection.EmulatorOrProduction:
bool anySet = allEmulatorEnvironmentVariables.Any(v => environment[v] is object);
if (!anySet)
{
return default;
}
bool allRequiredSet = requiredEmulatorEnvironmentVariables.All(v => environment[v] is object);
if (!allRequiredSet)
{
var sampleSet = allEmulatorEnvironmentVariables.First(v => environment[v] is object);
var sampleNotSet = requiredEmulatorEnvironmentVariables.First(v => environment[v] is null);
GaxPreconditions.CheckState(false,
"Emulator environment variable '{0}' is set, but '{1}' is not set.",
sampleSet, sampleNotSet);
}
// We allow other properties such as the endpoint to be set, although we expect them to be ignored
// by the calling code. This allows users to write code that has customizations in settings, but still doesn't need
// to be changed at all in order to use the emulator.
return environment;
default:
throw new InvalidOperationException($"Invalid emulator detection value: {EmulatorDetection}");
}

// Retrieves an environment variable from <see cref="EnvrionmentVariableProvider"/>, mapping empty or whitespace-only strings to null.
string GetEnvironmentVariableOrNull(string variable)
{
var value = environmentVariableProvider(variable);
return string.IsNullOrWhiteSpace(value) ? null : value;
}
}

// Copies common settings from the specified builder, expecting that any settings around
// credentials and endpoints will be set by the caller, along with any client-specific settings.
// Emulator detection is not copied, to avoid infinite recursion when building.
// This code is adapted from Google.Api.Gax.Grpc.ClientBuilderBase<T>.
private void CopySettingsForEmulator(StorageClientBuilder source)
{
GaxPreconditions.CheckNotNull(source, nameof(source));
// From ClientBuilderBase
HttpClientFactory = source.HttpClientFactory;
ApplicationName = source.ApplicationName;

// Storage-specific
EncryptionKey = source.EncryptionKey;
GZipEnabled = source.GZipEnabled;
Scheduler = source.Scheduler;
}
}
}
20 changes: 20 additions & 0 deletions apis/Google.Cloud.Storage.V1/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,23 @@ bucket. You'll need to add a dependency on the
manage its permissions.

{{sample:StorageClient.NotificationsOverview}}

## Connecting to an emulator

`StorageClient` supports the [Firebase Storage
Emulator](https://firebase.google.com/docs/emulator-suite/connect_storage).

This is configured using `StorageClientBuilder.EmulatorDetection`,
as described in the [client library help page on
emulators](https://cloud.google.com/dotnet/docs/reference/help/emulators).

As well as setting the `EmulatorDetection` property when building a
client, you must set the `STORAGE_EMULATOR_HOST` environment
variable, in the form `host:port` (or `http://host:port`), e.g.
`127.0.0.1:9199`. The port is the one shown in the emulator UI; note
that this is *not* the port of the Storage Emulator user interface.

Note that the Firebase Storage Emulator only supports a limited set
of operations. See [the Storage Emulator
documentation](https://firebase.google.com/docs/emulator-suite/connect_storage#differences_from_google_cloud_storage)
for more details.
3 changes: 2 additions & 1 deletion docs/devsite-help/emulators.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The packages for the following APIs support emulators directly in the way descri
- Bigtable
- Spanner
- Datastore
- Storage

In these packages, the client builder type has an `EmulatorDetection`
property which can be set to one of the following values:
Expand Down Expand Up @@ -91,4 +92,4 @@ switch that you should consider carefully before enabling. In most cases we expe
and appropriate when testing an application against an emulator, but that's not a decision
that the client libraries can reasonably take for themselves.

This switch is not required when running .NET 6.
This switch is not required when running .NET 6 or above.

0 comments on commit a454073

Please sign in to comment.