Skip to content

Add protocol as a separate property for FirebaseStorage service #5453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 8, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion common/api-review/storage.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export class _FirebaseStorageImpl implements FirebaseStorage {
// (undocumented)
readonly _pool: ConnectionPool;
// (undocumented)
protocol: string;
// (undocumented)
readonly _url?: string | undefined;
}

Expand Down Expand Up @@ -292,7 +294,7 @@ export interface UploadResult {
}

// @public
export function uploadString(ref: StorageReference, value: string, format?: string, metadata?: UploadMetadata): Promise<UploadResult>;
export function uploadString(ref: StorageReference, value: string, format?: StringFormat, metadata?: UploadMetadata): Promise<UploadResult>;

// @public
export interface UploadTask {
Expand Down
3 changes: 2 additions & 1 deletion packages/storage/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
} from './reference';
import { STORAGE_TYPE } from './constants';
import { EmulatorMockTokenOptions, getModularInstance } from '@firebase/util';
import { StringFormat } from './implementation/string';

export { EmulatorMockTokenOptions } from '@firebase/util';

Expand Down Expand Up @@ -109,7 +110,7 @@ export function uploadBytes(
export function uploadString(
ref: StorageReference,
value: string,
format?: string,
format?: StringFormat,
metadata?: UploadMetadata
): Promise<UploadResult> {
ref = getModularInstance(ref);
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 @@ -156,7 +156,8 @@ export function fromResourceString(
export function downloadUrlFromResourceString(
metadata: Metadata,
resourceString: string,
host: string
host: string,
protocol: string
): string | null {
const obj = jsonObjectOrNull(resourceString);
if (obj === null) {
Expand All @@ -177,7 +178,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, host);
const base = makeUrl(urlPart, host, protocol);
const queryString = makeQueryString({
alt: 'media',
token
Expand Down
27 changes: 11 additions & 16 deletions packages/storage/src/implementation/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export function downloadUrlHandler(
return downloadUrlFromResourceString(
metadata as Metadata,
text,
service.host
service.host,
service.protocol
);
}
return handler;
Expand All @@ -99,10 +100,7 @@ export function downloadUrlHandler(
export function sharedErrorHandler(
location: Location
): (p1: Connection, p2: StorageError) => StorageError {
function errorHandler(
xhr: Connection,
err: StorageError
): StorageError {
function errorHandler(xhr: Connection, err: StorageError): StorageError {
let newErr;
if (xhr.getStatus() === 401) {
if (
Expand Down Expand Up @@ -136,10 +134,7 @@ export function objectErrorHandler(
): (p1: Connection, p2: StorageError) => StorageError {
const shared = sharedErrorHandler(location);

function errorHandler(
xhr: Connection,
err: StorageError
): StorageError {
function errorHandler(xhr: Connection, err: StorageError): StorageError {
let newErr = shared(xhr, err);
if (xhr.getStatus() === 404) {
newErr = objectNotFound(location.path);
Expand All @@ -156,7 +151,7 @@ export function getMetadata(
mappings: Mappings
): RequestInfo<Metadata> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart, service.host);
const url = makeUrl(urlPart, service.host, service.protocol);
const method = 'GET';
const timeout = service.maxOperationRetryTime;
const requestInfo = new RequestInfo(
Expand Down Expand Up @@ -192,7 +187,7 @@ export function list(
urlParams['maxResults'] = maxResults;
}
const urlPart = location.bucketOnlyServerUrl();
const url = makeUrl(urlPart, service.host);
const url = makeUrl(urlPart, service.host, service.protocol);
const method = 'GET';
const timeout = service.maxOperationRetryTime;
const requestInfo = new RequestInfo(
Expand All @@ -212,7 +207,7 @@ export function getDownloadUrl(
mappings: Mappings
): RequestInfo<string | null> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart, service.host);
const url = makeUrl(urlPart, service.host, service.protocol);
const method = 'GET';
const timeout = service.maxOperationRetryTime;
const requestInfo = new RequestInfo(
Expand All @@ -232,7 +227,7 @@ export function updateMetadata(
mappings: Mappings
): RequestInfo<Metadata> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart, service.host);
const url = makeUrl(urlPart, service.host, service.protocol);
const method = 'PATCH';
const body = toResourceString(metadata, mappings);
const headers = { 'Content-Type': 'application/json; charset=utf-8' };
Expand All @@ -254,7 +249,7 @@ export function deleteObject(
location: Location
): RequestInfo<void> {
const urlPart = location.fullServerUrl();
const url = makeUrl(urlPart, service.host);
const url = makeUrl(urlPart, service.host, service.protocol);
const method = 'DELETE';
const timeout = service.maxOperationRetryTime;

Expand Down Expand Up @@ -334,7 +329,7 @@ export function multipartUpload(
throw cannotSliceBlob();
}
const urlParams: UrlParams = { name: metadata_['fullPath']! };
const url = makeUrl(urlPart, service.host);
const url = makeUrl(urlPart, service.host, service.protocol);
const method = 'POST';
const timeout = service.maxUploadRetryTime;
const requestInfo = new RequestInfo(
Expand Down Expand Up @@ -397,7 +392,7 @@ export function createResumableUpload(
const urlPart = location.bucketOnlyServerUrl();
const metadataForUpload = metadataForUpload_(location, blob, metadata);
const urlParams: UrlParams = { name: metadataForUpload['fullPath']! };
const url = makeUrl(urlPart, service.host);
const url = makeUrl(urlPart, service.host, service.protocol);
const method = 'POST';
const headers = {
'X-Goog-Upload-Protocol': 'resumable',
Expand Down
10 changes: 6 additions & 4 deletions packages/storage/src/implementation/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
*/
import { UrlParams } from './requestinfo';

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

export function makeQueryString(params: UrlParams): string {
Expand Down
10 changes: 5 additions & 5 deletions packages/storage/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ export function connectStorageEmulator(
mockUserToken?: EmulatorMockTokenOptions | string;
} = {}
): void {
storage.host = `http://${host}:${port}`;
storage.host = `${host}:${port}`;
storage.protocol = 'http';
const { mockUserToken } = options;
if (mockUserToken) {
storage._overrideAuthToken =
Expand All @@ -149,7 +150,7 @@ export function connectStorageEmulator(
/**
* A service that provides Firebase Storage Reference instances.
* @param opt_url - gs:// url to a custom Storage Bucket
*
*
* @internal
*/
export class FirebaseStorageImpl implements FirebaseStorage {
Expand All @@ -158,9 +159,9 @@ export class FirebaseStorageImpl implements FirebaseStorage {
* This string can be in the formats:
* - host
* - host:port
* - protocol://host:port
*/
private _host: string = DEFAULT_HOST;
protocol: string = 'https';
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this need to be exposed via the API?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This whole class is internal so it shouldn't be exposed publicly. But it can't be private since connectStorageEmulator needs to be able to set it. I can add an underscore so it's clear that it's not meant to be public?

Copy link
Contributor

Choose a reason for hiding this comment

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

The rest of the class does use underscores. I merely asked because of the change in the API report as it is not obvious that this is not exposed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added an underscore.

protected readonly _appId: string | null = null;
private readonly _requests: Set<Request<unknown>>;
private _deleted: boolean = false;
Expand Down Expand Up @@ -201,8 +202,7 @@ export class FirebaseStorageImpl implements FirebaseStorage {

/**
* Set host string for this service.
* @param host - host string in the form of host, host:port,
* or protocol://host:port
* @param host - host string in the form of host or host:port
*/
set host(host: string) {
this._host = host;
Expand Down
50 changes: 50 additions & 0 deletions packages/storage/test/unit/location.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect } from 'chai';
import { DEFAULT_HOST } from '../../src/implementation/constants';
import { Location } from '../../src/implementation/location';

describe('Firebase Storage > Location', () => {
it('makeFromUrl handles an emulator url correctly', () => {
const loc = Location.makeFromUrl(
'http://localhost:3001/v0/b/abcdefg.appspot.com/o/abcde.txt',
'localhost:3001'
);
expect(loc.bucket).to.equal('abcdefg.appspot.com');
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we test the protocol here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Location doesn't seem to store the host or the protocol, makeFromUrl just uses them to do regex matches and then throws them away. It only seems to have bucket, and path (just the part after the bucket).

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, got it.

});
it('makeFromUrl handles a Firebase Storage url correctly', () => {
const loc = Location.makeFromUrl(
'https://firebasestorage.googleapis.com/v0/b/abcdefgh.appspot.com/o/abcde.txt',
DEFAULT_HOST
);
expect(loc.bucket).to.equal('abcdefgh.appspot.com');
});
it('makeFromUrl handles a gs url correctly', () => {
const loc = Location.makeFromUrl(
'gs://mybucket/child/path/abcde.txt',
DEFAULT_HOST
);
expect(loc.bucket).to.equal('mybucket');
});
it('makeFromUrl handles a Cloud Storage url correctly', () => {
const loc = Location.makeFromUrl(
'https://storage.googleapis.com/mybucket/abcde.txt',
DEFAULT_HOST
);
expect(loc.bucket).to.equal('mybucket');
});
});
24 changes: 16 additions & 8 deletions packages/storage/test/unit/requests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe('Firebase Storage > Requests', () => {
const requestInfo = getMetadata(storageService, location, mappings);
assertObjectIncludes(
{
url: makeUrl(url, storageService.host),
url: makeUrl(url, storageService.host, storageService.protocol),
method: 'GET',
body: null,
headers: {},
Expand All @@ -222,7 +222,11 @@ describe('Firebase Storage > Requests', () => {
const requestInfo = list(storageService, locationRoot, '/');
assertObjectIncludes(
{
url: makeUrl(locationNormalNoObjUrl, storageService.host),
url: makeUrl(
locationNormalNoObjUrl,
storageService.host,
storageService.protocol
),
method: 'GET',
body: null,
headers: {},
Expand Down Expand Up @@ -252,7 +256,11 @@ describe('Firebase Storage > Requests', () => {
);
assertObjectIncludes(
{
url: makeUrl(locationNoObjectUrl, storageService.host),
url: makeUrl(
locationNoObjectUrl,
storageService.host,
storageService.protocol
),
method: 'GET',
body: null,
headers: {},
Expand Down Expand Up @@ -319,7 +327,7 @@ describe('Firebase Storage > Requests', () => {
const requestInfo = getDownloadUrl(storageService, location, mappings);
assertObjectIncludes(
{
url: makeUrl(url, storageService.host),
url: makeUrl(url, storageService.host, storageService.protocol),
method: 'GET',
body: null,
headers: {},
Expand Down Expand Up @@ -354,7 +362,7 @@ describe('Firebase Storage > Requests', () => {
);
assertObjectIncludes(
{
url: makeUrl(url, storageService.host),
url: makeUrl(url, storageService.host, storageService.protocol),
method: 'PATCH',
body: metadataString,
headers: { 'Content-Type': metadataContentType },
Expand Down Expand Up @@ -385,7 +393,7 @@ describe('Firebase Storage > Requests', () => {
const requestInfo = deleteObject(storageService, location);
assertObjectIncludes(
{
url: makeUrl(url, storageService.host),
url: makeUrl(url, storageService.host, storageService.protocol),
method: 'DELETE',
body: null,
headers: {},
Expand Down Expand Up @@ -451,7 +459,7 @@ describe('Firebase Storage > Requests', () => {

assertObjectIncludes(
{
url: makeUrl(url, storageService.host),
url: makeUrl(url, storageService.host, storageService.protocol),
method: 'POST',
urlParams: { name: location.path },
headers: {
Expand Down Expand Up @@ -496,7 +504,7 @@ describe('Firebase Storage > Requests', () => {
);
assertObjectIncludes(
{
url: makeUrl(url, storageService.host),
url: makeUrl(url, storageService.host, storageService.protocol),
method: 'POST',
urlParams: { name: location.path },
headers: {
Expand Down
13 changes: 7 additions & 6 deletions packages/storage/test/unit/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ GOOG4-RSA-SHA256`
testShared.makePool(newSend)
);
connectStorageEmulator(service, 'test.host.org', 1234);
expect(service.host).to.equal('http://test.host.org:1234');
expect(service.host).to.equal('test.host.org:1234');
expect(service.protocol).to.equal('http');
void getDownloadURL(ref(service, 'test.png'));
});
it('sets mock user token string if specified', done => {
Expand All @@ -283,7 +284,8 @@ GOOG4-RSA-SHA256`
testShared.makePool(newSend)
);
connectStorageEmulator(service, 'test.host.org', 1234, { mockUserToken });
expect(service.host).to.equal('http://test.host.org:1234');
expect(service.host).to.equal('test.host.org:1234');
expect(service.protocol).to.equal('http');
expect(service._overrideAuthToken).to.equal(mockUserToken);
void getDownloadURL(ref(service, 'test.png'));
});
Expand Down Expand Up @@ -313,7 +315,8 @@ GOOG4-RSA-SHA256`
connectStorageEmulator(service, 'test.host.org', 1234, {
mockUserToken: { sub: 'alice' }
});
expect(service.host).to.equal('http://test.host.org:1234');
expect(service.host).to.equal('test.host.org:1234');
expect(service.protocol).to.equal('http');
token = service._overrideAuthToken;
// Token should be an unsigned JWT with header { "alg": "none", "type": "JWT" } (base64url):
expect(token).to.match(/^eyJhbGciOiJub25lIiwidHlwZSI6IkpXVCJ9\./);
Expand Down Expand Up @@ -403,9 +406,7 @@ GOOG4-RSA-SHA256`
TaskEvent.STATE_CHANGED,
undefined,
(err: StorageError | Error) => {
expect((err as StorageError).code).to.equal(
'storage/app-deleted'
);
expect((err as StorageError).code).to.equal('storage/app-deleted');
resolve();
},
() => {
Expand Down