Skip to content

Commit 4a609d9

Browse files
authored
Merge ed256f5 into 2ed3291
2 parents 2ed3291 + ed256f5 commit 4a609d9

File tree

14 files changed

+183
-37
lines changed

14 files changed

+183
-37
lines changed

.changeset/tricky-seahorses-look.md

+8
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.

packages/firebase/index.d.ts

+7
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

+1
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

+11-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
*/
1717

1818
import * as types from '@firebase/storage-types';
19-
import { StorageService, isUrl, ref } from '../src/service';
19+
import {
20+
StorageService,
21+
isUrl,
22+
ref,
23+
useStorageEmulator as internalUseEmulator
24+
} from '../src/service';
2025
import { Location } from '../src/implementation/location';
2126
import { ReferenceCompat } from './reference';
2227
import { invalidArgument } from '../src/implementation/error';
@@ -70,7 +75,7 @@ export class StorageServiceCompat implements types.FirebaseStorage {
7075
);
7176
}
7277
try {
73-
Location.makeFromUrl(url);
78+
Location.makeFromUrl(url, this._delegate.host);
7479
} catch (e) {
7580
throw invalidArgument(
7681
'refFromUrl() expected a valid full URL but got an invalid one.'
@@ -86,4 +91,8 @@ export class StorageServiceCompat implements types.FirebaseStorage {
8691
setMaxOperationRetryTime(time: number): void {
8792
this._delegate.maxOperationRetryTime = time;
8893
}
94+
95+
useEmulator(host: string, port: number): void {
96+
internalUseEmulator(this._delegate, host, port);
97+
}
8998
}

packages/storage/exp/index.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import {
2727
import { XhrIoPool } from '../src/implementation/xhriopool';
2828
import {
2929
ref as refInternal,
30-
StorageService as StorageServiceInternal
30+
StorageService as StorageServiceInternal,
31+
useStorageEmulator as useEmulatorInternal
3132
} from '../src/service';
3233
import {
3334
Component,
@@ -265,6 +266,22 @@ export function ref(
265266
);
266267
}
267268

269+
/**
270+
* Modify this `StorageService` instance to communicate with the Cloud Storage emulator.
271+
*
272+
* @param storage - The `StorageService` instance
273+
* @param host - The emulator host (ex: localhost)
274+
* @param port - The emulator port (ex: 5001)
275+
* @public
276+
*/
277+
export function useStorageEmulator(
278+
storage: StorageService,
279+
host: string,
280+
port: number
281+
): void {
282+
useEmulatorInternal(storage as StorageServiceInternal, host, port);
283+
}
284+
268285
export { StringFormat } from '../src/implementation/string';
269286

270287
/**

packages/storage/src/implementation/location.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ export class Location {
5151
return '/b/' + encode(this.bucket) + '/o';
5252
}
5353

54-
static makeFromBucketSpec(bucketString: string): Location {
54+
static makeFromBucketSpec(bucketString: string, host: string): Location {
5555
let bucketLocation;
5656
try {
57-
bucketLocation = Location.makeFromUrl(bucketString);
57+
bucketLocation = Location.makeFromUrl(bucketString, host);
5858
} catch (e) {
5959
// Not valid URL, use as-is. This lets you put bare bucket names in
6060
// config.
@@ -67,7 +67,7 @@ export class Location {
6767
}
6868
}
6969

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

@@ -84,7 +84,7 @@ export class Location {
8484
loc.path_ = decodeURIComponent(loc.path);
8585
}
8686
const version = 'v[A-Za-z0-9_]+';
87-
const firebaseStorageHost = DEFAULT_HOST.replace(/[.]/g, '\\.');
87+
const firebaseStorageHost = host.replace(/[.]/g, '\\.');
8888
const firebaseStoragePath = '(/([^?#]*).*)?$';
8989
const firebaseStorageRegExp = new RegExp(
9090
`^https?://${firebaseStorageHost}/${version}/b/${bucketDomain}/o${firebaseStoragePath}`,
@@ -93,7 +93,9 @@ export class Location {
9393
const firebaseStorageIndices = { bucket: 1, path: 3 };
9494

9595
const cloudStorageHost =
96-
'(?:storage.googleapis.com|storage.cloud.google.com)';
96+
host === DEFAULT_HOST
97+
? '(?:storage.googleapis.com|storage.cloud.google.com)'
98+
: host;
9799
const cloudStoragePath = '([^?#]*)';
98100
const cloudStorageRegExp = new RegExp(
99101
`^https?://${cloudStorageHost}/${bucketDomain}/${cloudStoragePath}`,

packages/storage/src/implementation/metadata.ts

+3-2
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

+12-8
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

+8-3
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

+1-1
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

+42-5
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)