Skip to content

Commit 5ae7365

Browse files
authored
Add useEmulator() to Storage (#4346)
1 parent 129888c commit 5ae7365

File tree

16 files changed

+193
-42
lines changed

16 files changed

+193
-42
lines changed

.changeset/tricky-seahorses-look.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'firebase': minor
3+
'@firebase/storage': minor
4+
'@firebase/storage-types': minor
5+
---
6+
7+
Add `storage().useEmulator()` method to enable emulator mode for storage, allowing users
8+
to set a storage emulator host and port.

common/api-review/storage.api.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function getDownloadURL(ref: StorageReference): Promise<string>;
6161
export function getMetadata(ref: StorageReference): Promise<FullMetadata>;
6262

6363
// @public
64-
export function getStorage(app: FirebaseApp, bucketUrl?: string): StorageService;
64+
export function getStorage(app?: FirebaseApp, bucketUrl?: string): StorageService;
6565

6666
// @public
6767
export function list(ref: StorageReference, options?: ListOptions): Promise<ListResult>;
@@ -94,9 +94,9 @@ export class _Location {
9494
// (undocumented)
9595
get isRoot(): boolean;
9696
// (undocumented)
97-
static makeFromBucketSpec(bucketString: string): _Location;
97+
static makeFromBucketSpec(bucketString: string, host: string): _Location;
9898
// (undocumented)
99-
static makeFromUrl(url: string): _Location;
99+
static makeFromUrl(url: string, host: string): _Location;
100100
// (undocumented)
101101
get path(): string;
102102
}
@@ -253,6 +253,9 @@ export interface UploadTaskSnapshot {
253253
totalBytes: number;
254254
}
255255

256+
// @public
257+
export function useStorageEmulator(storage: StorageService, host: string, port: number): void;
258+
256259

257260
// (No @packageDocumentation comment for this package)
258261

packages/firebase/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7612,6 +7612,13 @@ declare namespace firebase.storage {
76127612
* @see {@link firebase.storage.Storage.maxUploadRetryTime}
76137613
*/
76147614
setMaxUploadRetryTime(time: number): any;
7615+
/**
7616+
* Modify this `Storage` instance to communicate with the Cloud Storage emulator.
7617+
*
7618+
* @param host - The emulator host (ex: localhost)
7619+
* @param port - The emulator port (ex: 5001)
7620+
*/
7621+
useEmulator(host: string, port: string): void;
76157622
}
76167623

76177624
/**

packages/storage-types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export class FirebaseStorage {
135135
refFromURL(url: string): Reference;
136136
setMaxOperationRetryTime(time: number): void;
137137
setMaxUploadRetryTime(time: number): void;
138+
useEmulator(host: string, port: number): void;
138139
}
139140

140141
declare module '@firebase/component' {

packages/storage/compat/service.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@
1818
import * as types from '@firebase/storage-types';
1919
import { FirebaseApp } from '@firebase/app-types';
2020

21-
import { StorageService, ref, _Location } from '../exp/api'; // import from the exp public API
21+
import { ref, _Location } from '../exp/api'; // import from the exp public API
2222
import { ReferenceCompat } from './reference';
23-
import { isUrl } from '../src/service';
23+
import {
24+
isUrl,
25+
StorageService,
26+
useStorageEmulator as internalUseEmulator
27+
} from '../src/service';
2428
import { invalidArgument } from '../src/implementation/error';
2529
import { Compat } from '@firebase/util';
2630

@@ -73,7 +77,7 @@ export class StorageServiceCompat
7377
);
7478
}
7579
try {
76-
_Location.makeFromUrl(url);
80+
_Location.makeFromUrl(url, this._delegate.host);
7781
} catch (e) {
7882
throw invalidArgument(
7983
'refFromUrl() expected a valid full URL but got an invalid one.'
@@ -89,4 +93,8 @@ export class StorageServiceCompat
8993
setMaxOperationRetryTime(time: number): void {
9094
this._delegate.maxOperationRetryTime = time;
9195
}
96+
97+
useEmulator(host: string, port: number): void {
98+
internalUseEmulator(this._delegate, host, port);
99+
}
92100
}

packages/storage/exp/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import {
2323
} from '@firebase/app-exp';
2424

2525
import { XhrIoPool } from '../src/implementation/xhriopool';
26-
import { StorageService as StorageServiceInternal } from '../src/service';
26+
import {
27+
StorageService as StorageServiceInternal,
28+
useStorageEmulator as useEmulatorInternal
29+
} from '../src/service';
2730
import {
2831
Component,
2932
ComponentType,
@@ -36,6 +39,23 @@ import { name, version } from '../package.json';
3639
import { StorageService } from './public-types';
3740
import { STORAGE_TYPE } from './constants';
3841

42+
/**
43+
* Modify this `StorageService` instance to communicate with the Cloud Storage emulator.
44+
*
45+
* @param storage - The `StorageService` instance
46+
* @param host - The emulator host (ex: localhost)
47+
* @param port - The emulator port (ex: 5001)
48+
* @public
49+
*/
50+
export function useStorageEmulator(
51+
storage: StorageService,
52+
host: string,
53+
port: number
54+
): void {
55+
useEmulatorInternal(storage as StorageServiceInternal, host, port);
56+
}
57+
58+
export { StringFormat } from '../src/implementation/string';
3959
export * from './api';
4060

4161
function factory(

packages/storage/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,4 @@
6363
"url": "https://github.com/firebase/firebase-js-sdk/issues"
6464
},
6565
"typings": "dist/index.d.ts"
66-
}
66+
}

packages/storage/src/implementation/location.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ export class Location {
5353
return '/b/' + encode(this.bucket) + '/o';
5454
}
5555

56-
static makeFromBucketSpec(bucketString: string): Location {
56+
static makeFromBucketSpec(bucketString: string, host: string): Location {
5757
let bucketLocation;
5858
try {
59-
bucketLocation = Location.makeFromUrl(bucketString);
59+
bucketLocation = Location.makeFromUrl(bucketString, host);
6060
} catch (e) {
6161
// Not valid URL, use as-is. This lets you put bare bucket names in
6262
// config.
@@ -69,7 +69,7 @@ export class Location {
6969
}
7070
}
7171

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

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

9797
const cloudStorageHost =
98-
'(?:storage.googleapis.com|storage.cloud.google.com)';
98+
host === DEFAULT_HOST
99+
? '(?:storage.googleapis.com|storage.cloud.google.com)'
100+
: host;
99101
const cloudStoragePath = '([^?#]*)';
100102
const cloudStorageRegExp = new RegExp(
101103
`^https?://${cloudStorageHost}/${bucketDomain}/${cloudStoragePath}`,

packages/storage/src/implementation/metadata.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ export function fromResourceString(
155155

156156
export function downloadUrlFromResourceString(
157157
metadata: Metadata,
158-
resourceString: string
158+
resourceString: string,
159+
host: string
159160
): string | null {
160161
const obj = jsonObjectOrNull(resourceString);
161162
if (obj === null) {
@@ -176,7 +177,7 @@ export function downloadUrlFromResourceString(
176177
const bucket: string = metadata['bucket'] as string;
177178
const path: string = metadata['fullPath'] as string;
178179
const urlPart = '/b/' + encode(bucket) + '/o/' + encode(path);
179-
const base = makeUrl(urlPart);
180+
const base = makeUrl(urlPart, host);
180181
const queryString = makeQueryString({
181182
alt: 'media',
182183
token

packages/storage/src/implementation/requests.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ export function downloadUrlHandler(
8686
function handler(xhr: XhrIo, text: string): string | null {
8787
const metadata = fromResourceString(service, text, mappings);
8888
handlerCheck(metadata !== null);
89-
return downloadUrlFromResourceString(metadata as Metadata, text);
89+
return downloadUrlFromResourceString(
90+
metadata as Metadata,
91+
text,
92+
service.host
93+
);
9094
}
9195
return handler;
9296
}
@@ -143,7 +147,7 @@ export function getMetadata(
143147
mappings: Mappings
144148
): RequestInfo<Metadata> {
145149
const urlPart = location.fullServerUrl();
146-
const url = makeUrl(urlPart);
150+
const url = makeUrl(urlPart, service.host);
147151
const method = 'GET';
148152
const timeout = service.maxOperationRetryTime;
149153
const requestInfo = new RequestInfo(
@@ -179,7 +183,7 @@ export function list(
179183
urlParams['maxResults'] = maxResults;
180184
}
181185
const urlPart = location.bucketOnlyServerUrl();
182-
const url = makeUrl(urlPart);
186+
const url = makeUrl(urlPart, service.host);
183187
const method = 'GET';
184188
const timeout = service.maxOperationRetryTime;
185189
const requestInfo = new RequestInfo(
@@ -199,7 +203,7 @@ export function getDownloadUrl(
199203
mappings: Mappings
200204
): RequestInfo<string | null> {
201205
const urlPart = location.fullServerUrl();
202-
const url = makeUrl(urlPart);
206+
const url = makeUrl(urlPart, service.host);
203207
const method = 'GET';
204208
const timeout = service.maxOperationRetryTime;
205209
const requestInfo = new RequestInfo(
@@ -219,7 +223,7 @@ export function updateMetadata(
219223
mappings: Mappings
220224
): RequestInfo<Metadata> {
221225
const urlPart = location.fullServerUrl();
222-
const url = makeUrl(urlPart);
226+
const url = makeUrl(urlPart, service.host);
223227
const method = 'PATCH';
224228
const body = toResourceString(metadata, mappings);
225229
const headers = { 'Content-Type': 'application/json; charset=utf-8' };
@@ -241,7 +245,7 @@ export function deleteObject(
241245
location: Location
242246
): RequestInfo<void> {
243247
const urlPart = location.fullServerUrl();
244-
const url = makeUrl(urlPart);
248+
const url = makeUrl(urlPart, service.host);
245249
const method = 'DELETE';
246250
const timeout = service.maxOperationRetryTime;
247251

@@ -321,7 +325,7 @@ export function multipartUpload(
321325
throw cannotSliceBlob();
322326
}
323327
const urlParams: UrlParams = { name: metadata_['fullPath']! };
324-
const url = makeUrl(urlPart);
328+
const url = makeUrl(urlPart, service.host);
325329
const method = 'POST';
326330
const timeout = service.maxUploadRetryTime;
327331
const requestInfo = new RequestInfo(
@@ -381,7 +385,7 @@ export function createResumableUpload(
381385
const urlPart = location.bucketOnlyServerUrl();
382386
const metadataForUpload = metadataForUpload_(location, blob, metadata);
383387
const urlParams: UrlParams = { name: metadataForUpload['fullPath']! };
384-
const url = makeUrl(urlPart);
388+
const url = makeUrl(urlPart, service.host);
385389
const method = 'POST';
386390
const headers = {
387391
'X-Goog-Upload-Protocol': 'resumable',

packages/storage/src/implementation/url.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@
1818
/**
1919
* @fileoverview Functions to create and manipulate URLs for the server API.
2020
*/
21-
import { DEFAULT_HOST } from './constants';
2221
import { UrlParams } from './requestinfo';
2322

24-
export function makeUrl(urlPart: string): string {
25-
return `https://${DEFAULT_HOST}/v0${urlPart}`;
23+
export function makeUrl(urlPart: string, host: string): string {
24+
const protocolMatch = host.match(/^(\w+):\/\/.+/);
25+
const protocol = protocolMatch?.[1];
26+
let origin = host;
27+
if (protocol == null) {
28+
origin = `https://${host}`;
29+
}
30+
return `${origin}/v0${urlPart}`;
2631
}
2732

2833
export function makeQueryString(params: UrlParams): string {

packages/storage/src/reference.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class Reference {
6060
if (location instanceof Location) {
6161
this._location = location;
6262
} else {
63-
this._location = Location.makeFromUrl(location);
63+
this._location = Location.makeFromUrl(location, _service.host);
6464
}
6565
}
6666

packages/storage/src/service.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from '@firebase/app-exp';
3232
import {
3333
CONFIG_STORAGE_BUCKET_KEY,
34+
DEFAULT_HOST,
3435
DEFAULT_MAX_OPERATION_RETRY_TIME,
3536
DEFAULT_MAX_UPLOAD_RETRY_TIME
3637
} from '../src/implementation/constants';
@@ -120,12 +121,23 @@ export function ref(
120121
}
121122
}
122123

123-
function extractBucket(config?: FirebaseOptions): Location | null {
124+
function extractBucket(
125+
host: string,
126+
config?: FirebaseOptions
127+
): Location | null {
124128
const bucketString = config?.[CONFIG_STORAGE_BUCKET_KEY];
125129
if (bucketString == null) {
126130
return null;
127131
}
128-
return Location.makeFromBucketSpec(bucketString);
132+
return Location.makeFromBucketSpec(bucketString, host);
133+
}
134+
135+
export function useStorageEmulator(
136+
storage: StorageService,
137+
host: string,
138+
port: number
139+
): void {
140+
storage.host = `http://${host}:${port}`;
129141
}
130142

131143
/**
@@ -134,7 +146,14 @@ function extractBucket(config?: FirebaseOptions): Location | null {
134146
* @param opt_url - gs:// url to a custom Storage Bucket
135147
*/
136148
export class StorageService implements _FirebaseService {
137-
readonly _bucket: Location | null = null;
149+
_bucket: Location | null = null;
150+
/**
151+
* This string can be in the formats:
152+
* - host
153+
* - host:port
154+
* - protocol://host:port
155+
*/
156+
private _host: string = DEFAULT_HOST;
138157
protected readonly _appId: string | null = null;
139158
private readonly _requests: Set<Request<unknown>>;
140159
private _deleted: boolean = false;
@@ -155,9 +174,27 @@ export class StorageService implements _FirebaseService {
155174
this._maxUploadRetryTime = DEFAULT_MAX_UPLOAD_RETRY_TIME;
156175
this._requests = new Set();
157176
if (_url != null) {
158-
this._bucket = Location.makeFromBucketSpec(_url);
177+
this._bucket = Location.makeFromBucketSpec(_url, this._host);
178+
} else {
179+
this._bucket = extractBucket(this._host, this.app.options);
180+
}
181+
}
182+
183+
get host(): string {
184+
return this._host;
185+
}
186+
187+
/**
188+
* Set host string for this service.
189+
* @param host - host string in the form of host, host:port,
190+
* or protocol://host:port
191+
*/
192+
set host(host: string) {
193+
this._host = host;
194+
if (this._url != null) {
195+
this._bucket = Location.makeFromBucketSpec(this._url, host);
159196
} else {
160-
this._bucket = extractBucket(this.app.options);
197+
this._bucket = extractBucket(host, this.app.options);
161198
}
162199
}
163200

0 commit comments

Comments
 (0)