Skip to content

Commit

Permalink
Add useEmulator() to Storage (#4346)
Browse files Browse the repository at this point in the history
  • Loading branch information
hsubox76 committed Apr 7, 2021
1 parent 129888c commit 5ae7365
Show file tree
Hide file tree
Showing 16 changed files with 193 additions and 42 deletions.
8 changes: 8 additions & 0 deletions .changeset/tricky-seahorses-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'firebase': minor
'@firebase/storage': minor
'@firebase/storage-types': minor
---

Add `storage().useEmulator()` method to enable emulator mode for storage, allowing users
to set a storage emulator host and port.
9 changes: 6 additions & 3 deletions common/api-review/storage.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function getDownloadURL(ref: StorageReference): Promise<string>;
export function getMetadata(ref: StorageReference): Promise<FullMetadata>;

// @public
export function getStorage(app: FirebaseApp, bucketUrl?: string): StorageService;
export function getStorage(app?: FirebaseApp, bucketUrl?: string): StorageService;

// @public
export function list(ref: StorageReference, options?: ListOptions): Promise<ListResult>;
Expand Down Expand Up @@ -94,9 +94,9 @@ export class _Location {
// (undocumented)
get isRoot(): boolean;
// (undocumented)
static makeFromBucketSpec(bucketString: string): _Location;
static makeFromBucketSpec(bucketString: string, host: string): _Location;
// (undocumented)
static makeFromUrl(url: string): _Location;
static makeFromUrl(url: string, host: string): _Location;
// (undocumented)
get path(): string;
}
Expand Down Expand Up @@ -253,6 +253,9 @@ export interface UploadTaskSnapshot {
totalBytes: number;
}

// @public
export function useStorageEmulator(storage: StorageService, host: string, port: number): void;


// (No @packageDocumentation comment for this package)

Expand Down
7 changes: 7 additions & 0 deletions packages/firebase/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7612,6 +7612,13 @@ declare namespace firebase.storage {
* @see {@link firebase.storage.Storage.maxUploadRetryTime}
*/
setMaxUploadRetryTime(time: number): any;
/**
* Modify this `Storage` instance to communicate with the Cloud Storage emulator.
*
* @param host - The emulator host (ex: localhost)
* @param port - The emulator port (ex: 5001)
*/
useEmulator(host: string, port: string): void;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/storage-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export class FirebaseStorage {
refFromURL(url: string): Reference;
setMaxOperationRetryTime(time: number): void;
setMaxUploadRetryTime(time: number): void;
useEmulator(host: string, port: number): void;
}

declare module '@firebase/component' {
Expand Down
14 changes: 11 additions & 3 deletions packages/storage/compat/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
import * as types from '@firebase/storage-types';
import { FirebaseApp } from '@firebase/app-types';

import { StorageService, ref, _Location } from '../exp/api'; // import from the exp public API
import { ref, _Location } from '../exp/api'; // import from the exp public API
import { ReferenceCompat } from './reference';
import { isUrl } from '../src/service';
import {
isUrl,
StorageService,
useStorageEmulator as internalUseEmulator
} from '../src/service';
import { invalidArgument } from '../src/implementation/error';
import { Compat } from '@firebase/util';

Expand Down Expand Up @@ -73,7 +77,7 @@ export class StorageServiceCompat
);
}
try {
_Location.makeFromUrl(url);
_Location.makeFromUrl(url, this._delegate.host);
} catch (e) {
throw invalidArgument(
'refFromUrl() expected a valid full URL but got an invalid one.'
Expand All @@ -89,4 +93,8 @@ export class StorageServiceCompat
setMaxOperationRetryTime(time: number): void {
this._delegate.maxOperationRetryTime = time;
}

useEmulator(host: string, port: number): void {
internalUseEmulator(this._delegate, host, port);
}
}
22 changes: 21 additions & 1 deletion packages/storage/exp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import {
} from '@firebase/app-exp';

import { XhrIoPool } from '../src/implementation/xhriopool';
import { StorageService as StorageServiceInternal } from '../src/service';
import {
StorageService as StorageServiceInternal,
useStorageEmulator as useEmulatorInternal
} from '../src/service';
import {
Component,
ComponentType,
Expand All @@ -36,6 +39,23 @@ import { name, version } from '../package.json';
import { StorageService } from './public-types';
import { STORAGE_TYPE } from './constants';

/**
* Modify this `StorageService` instance to communicate with the Cloud Storage emulator.
*
* @param storage - The `StorageService` instance
* @param host - The emulator host (ex: localhost)
* @param port - The emulator port (ex: 5001)
* @public
*/
export function useStorageEmulator(
storage: StorageService,
host: string,
port: number
): void {
useEmulatorInternal(storage as StorageServiceInternal, host, port);
}

export { StringFormat } from '../src/implementation/string';
export * from './api';

function factory(
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@
"url": "https://github.com/firebase/firebase-js-sdk/issues"
},
"typings": "dist/index.d.ts"
}
}
12 changes: 7 additions & 5 deletions packages/storage/src/implementation/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ export class Location {
return '/b/' + encode(this.bucket) + '/o';
}

static makeFromBucketSpec(bucketString: string): Location {
static makeFromBucketSpec(bucketString: string, host: string): Location {
let bucketLocation;
try {
bucketLocation = Location.makeFromUrl(bucketString);
bucketLocation = Location.makeFromUrl(bucketString, host);
} catch (e) {
// Not valid URL, use as-is. This lets you put bare bucket names in
// config.
Expand All @@ -69,7 +69,7 @@ export class Location {
}
}

static makeFromUrl(url: string): Location {
static makeFromUrl(url: string, host: string): Location {
let location: Location | null = null;
const bucketDomain = '([A-Za-z0-9.\\-_]+)';

Expand All @@ -86,7 +86,7 @@ export class Location {
loc.path_ = decodeURIComponent(loc.path);
}
const version = 'v[A-Za-z0-9_]+';
const firebaseStorageHost = DEFAULT_HOST.replace(/[.]/g, '\\.');
const firebaseStorageHost = host.replace(/[.]/g, '\\.');
const firebaseStoragePath = '(/([^?#]*).*)?$';
const firebaseStorageRegExp = new RegExp(
`^https?://${firebaseStorageHost}/${version}/b/${bucketDomain}/o${firebaseStoragePath}`,
Expand All @@ -95,7 +95,9 @@ export class Location {
const firebaseStorageIndices = { bucket: 1, path: 3 };

const cloudStorageHost =
'(?:storage.googleapis.com|storage.cloud.google.com)';
host === DEFAULT_HOST
? '(?:storage.googleapis.com|storage.cloud.google.com)'
: host;
const cloudStoragePath = '([^?#]*)';
const cloudStorageRegExp = new RegExp(
`^https?://${cloudStorageHost}/${bucketDomain}/${cloudStoragePath}`,
Expand Down
5 changes: 3 additions & 2 deletions packages/storage/src/implementation/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ export function fromResourceString(

export function downloadUrlFromResourceString(
metadata: Metadata,
resourceString: string
resourceString: string,
host: string
): string | null {
const obj = jsonObjectOrNull(resourceString);
if (obj === null) {
Expand All @@ -176,7 +177,7 @@ export function downloadUrlFromResourceString(
const bucket: string = metadata['bucket'] as string;
const path: string = metadata['fullPath'] as string;
const urlPart = '/b/' + encode(bucket) + '/o/' + encode(path);
const base = makeUrl(urlPart);
const base = makeUrl(urlPart, host);
const queryString = makeQueryString({
alt: 'media',
token
Expand Down
20 changes: 12 additions & 8 deletions packages/storage/src/implementation/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ export function downloadUrlHandler(
function handler(xhr: XhrIo, text: string): string | null {
const metadata = fromResourceString(service, text, mappings);
handlerCheck(metadata !== null);
return downloadUrlFromResourceString(metadata as Metadata, text);
return downloadUrlFromResourceString(
metadata as Metadata,
text,
service.host
);
}
return handler;
}
Expand Down Expand Up @@ -143,7 +147,7 @@ export function getMetadata(
mappings: Mappings
): RequestInfo<Metadata> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart);
const url = makeUrl(urlPart, service.host);
const method = 'GET';
const timeout = service.maxOperationRetryTime;
const requestInfo = new RequestInfo(
Expand Down Expand Up @@ -179,7 +183,7 @@ export function list(
urlParams['maxResults'] = maxResults;
}
const urlPart = location.bucketOnlyServerUrl();
const url = makeUrl(urlPart);
const url = makeUrl(urlPart, service.host);
const method = 'GET';
const timeout = service.maxOperationRetryTime;
const requestInfo = new RequestInfo(
Expand All @@ -199,7 +203,7 @@ export function getDownloadUrl(
mappings: Mappings
): RequestInfo<string | null> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart);
const url = makeUrl(urlPart, service.host);
const method = 'GET';
const timeout = service.maxOperationRetryTime;
const requestInfo = new RequestInfo(
Expand All @@ -219,7 +223,7 @@ export function updateMetadata(
mappings: Mappings
): RequestInfo<Metadata> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart);
const url = makeUrl(urlPart, service.host);
const method = 'PATCH';
const body = toResourceString(metadata, mappings);
const headers = { 'Content-Type': 'application/json; charset=utf-8' };
Expand All @@ -241,7 +245,7 @@ export function deleteObject(
location: Location
): RequestInfo<void> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart);
const url = makeUrl(urlPart, service.host);
const method = 'DELETE';
const timeout = service.maxOperationRetryTime;

Expand Down Expand Up @@ -321,7 +325,7 @@ export function multipartUpload(
throw cannotSliceBlob();
}
const urlParams: UrlParams = { name: metadata_['fullPath']! };
const url = makeUrl(urlPart);
const url = makeUrl(urlPart, service.host);
const method = 'POST';
const timeout = service.maxUploadRetryTime;
const requestInfo = new RequestInfo(
Expand Down Expand Up @@ -381,7 +385,7 @@ export function createResumableUpload(
const urlPart = location.bucketOnlyServerUrl();
const metadataForUpload = metadataForUpload_(location, blob, metadata);
const urlParams: UrlParams = { name: metadataForUpload['fullPath']! };
const url = makeUrl(urlPart);
const url = makeUrl(urlPart, service.host);
const method = 'POST';
const headers = {
'X-Goog-Upload-Protocol': 'resumable',
Expand Down
11 changes: 8 additions & 3 deletions packages/storage/src/implementation/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@
/**
* @fileoverview Functions to create and manipulate URLs for the server API.
*/
import { DEFAULT_HOST } from './constants';
import { UrlParams } from './requestinfo';

export function makeUrl(urlPart: string): string {
return `https://${DEFAULT_HOST}/v0${urlPart}`;
export function makeUrl(urlPart: string, host: string): string {
const protocolMatch = host.match(/^(\w+):\/\/.+/);
const protocol = protocolMatch?.[1];
let origin = host;
if (protocol == null) {
origin = `https://${host}`;
}
return `${origin}/v0${urlPart}`;
}

export function makeQueryString(params: UrlParams): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/storage/src/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class Reference {
if (location instanceof Location) {
this._location = location;
} else {
this._location = Location.makeFromUrl(location);
this._location = Location.makeFromUrl(location, _service.host);
}
}

Expand Down
47 changes: 42 additions & 5 deletions packages/storage/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@firebase/app-exp';
import {
CONFIG_STORAGE_BUCKET_KEY,
DEFAULT_HOST,
DEFAULT_MAX_OPERATION_RETRY_TIME,
DEFAULT_MAX_UPLOAD_RETRY_TIME
} from '../src/implementation/constants';
Expand Down Expand Up @@ -120,12 +121,23 @@ export function ref(
}
}

function extractBucket(config?: FirebaseOptions): Location | null {
function extractBucket(
host: string,
config?: FirebaseOptions
): Location | null {
const bucketString = config?.[CONFIG_STORAGE_BUCKET_KEY];
if (bucketString == null) {
return null;
}
return Location.makeFromBucketSpec(bucketString);
return Location.makeFromBucketSpec(bucketString, host);
}

export function useStorageEmulator(
storage: StorageService,
host: string,
port: number
): void {
storage.host = `http://${host}:${port}`;
}

/**
Expand All @@ -134,7 +146,14 @@ function extractBucket(config?: FirebaseOptions): Location | null {
* @param opt_url - gs:// url to a custom Storage Bucket
*/
export class StorageService implements _FirebaseService {
readonly _bucket: Location | null = null;
_bucket: Location | null = null;
/**
* This string can be in the formats:
* - host
* - host:port
* - protocol://host:port
*/
private _host: string = DEFAULT_HOST;
protected readonly _appId: string | null = null;
private readonly _requests: Set<Request<unknown>>;
private _deleted: boolean = false;
Expand All @@ -155,9 +174,27 @@ export class StorageService implements _FirebaseService {
this._maxUploadRetryTime = DEFAULT_MAX_UPLOAD_RETRY_TIME;
this._requests = new Set();
if (_url != null) {
this._bucket = Location.makeFromBucketSpec(_url);
this._bucket = Location.makeFromBucketSpec(_url, this._host);
} else {
this._bucket = extractBucket(this._host, this.app.options);
}
}

get host(): string {
return this._host;
}

/**
* Set host string for this service.
* @param host - host string in the form of host, host:port,
* or protocol://host:port
*/
set host(host: string) {
this._host = host;
if (this._url != null) {
this._bucket = Location.makeFromBucketSpec(this._url, host);
} else {
this._bucket = extractBucket(this.app.options);
this._bucket = extractBucket(host, this.app.options);
}
}

Expand Down
Loading

0 comments on commit 5ae7365

Please sign in to comment.