Skip to content

Commit 8ed5fb4

Browse files
Add getBlob
1 parent 2e29beb commit 8ed5fb4

File tree

13 files changed

+287
-14
lines changed

13 files changed

+287
-14
lines changed

packages/storage/exp/api.browser.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import {
19+
StorageReference,
20+
} from './public-types';
21+
import {
22+
Reference,
23+
getBlobInternal
24+
} from '../src/reference';
25+
import { getModularInstance } from '@firebase/util';
26+
27+
/**
28+
* Downloads the data at the object's location. Returns an error if the object
29+
* is not found.
30+
*
31+
* To use this functionality, you have to whitelist your app's origin in your
32+
* Cloud Storage bucket. See also
33+
* https://cloud.google.com/storage/docs/configuring-cors
34+
*
35+
* @public
36+
* @param ref - StorageReference where data should be download.
37+
* @returns A Promise that resolves with a Blob containing the object's bytes
38+
*/
39+
export function getBlob(ref: StorageReference): Promise<Blob> {
40+
ref = getModularInstance(ref);
41+
return getBlobInternal(ref as Reference);
42+
}
43+

packages/storage/exp/index.node.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Cloud Storage for Firebase
3+
*
4+
* @packageDocumentation
5+
*/
6+
7+
/**
8+
* @license
9+
* Copyright 2021 Google LLC
10+
*
11+
* Licensed under the Apache License, Version 2.0 (the "License");
12+
* you may not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing, software
18+
* distributed under the License is distributed on an "AS IS" BASIS,
19+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20+
* See the License for the specific language governing permissions and
21+
* limitations under the License.
22+
*/
23+
24+
// eslint-disable-next-line import/no-extraneous-dependencies
25+
import {
26+
_registerComponent,
27+
registerVersion,
28+
SDK_VERSION
29+
} from '@firebase/app-exp';
30+
31+
import { FirebaseStorageImpl } from '../src/service';
32+
import {
33+
Component,
34+
ComponentType,
35+
ComponentContainer,
36+
InstanceFactoryOptions
37+
} from '@firebase/component';
38+
39+
import { name, version } from '../package.json';
40+
41+
import { FirebaseStorage } from './public-types';
42+
import { STORAGE_TYPE } from './constants';
43+
44+
export { StringFormat } from '../src/implementation/string';
45+
export * from './api';
46+
47+
function factory(
48+
container: ComponentContainer,
49+
{ instanceIdentifier: url }: InstanceFactoryOptions
50+
): FirebaseStorage {
51+
const app = container.getProvider('app-exp').getImmediate();
52+
const authProvider = container.getProvider('auth-internal');
53+
const appCheckProvider = container.getProvider('app-check-internal');
54+
55+
return new FirebaseStorageImpl(
56+
app,
57+
authProvider,
58+
appCheckProvider,
59+
url,
60+
SDK_VERSION
61+
);
62+
}
63+
64+
function registerStorage(): void {
65+
_registerComponent(
66+
new Component(
67+
STORAGE_TYPE,
68+
factory,
69+
ComponentType.PUBLIC
70+
).setMultipleInstances(true)
71+
);
72+
73+
registerVersion(name, version);
74+
}
75+
76+
registerStorage();

packages/storage/exp/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { STORAGE_TYPE } from './constants';
4242

4343
export { StringFormat } from '../src/implementation/string';
4444
export * from './api';
45+
export * from './api.browser';
4546

4647
function factory(
4748
container: ComponentContainer,

packages/storage/karma.conf.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function getTestFiles(argv) {
4444
);
4545
integrationTestFiles = ['test/integration/*compat*'];
4646
} else {
47-
integrationTestFiles = ['test/integration/*'];
47+
integrationTestFiles = ['test/integration/*', 'test/browser/*'];
4848
}
4949
if (argv.unit) {
5050
return unitTestFiles;

packages/storage/rollup.config.exp.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const es2017Plugins = [
7676
const es2017Builds = [
7777
// Node
7878
{
79-
input: './exp/index.ts',
79+
input: './exp/index.node.ts',
8080
output: {
8181
file: path.resolve('./exp', pkgExp.main),
8282
format: 'cjs',

packages/storage/src/implementation/requests.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,23 @@ export function getBytesHandler(): RequestHandler<ArrayBuffer, ArrayBuffer> {
223223
return (xhr: Connection<ArrayBuffer>, data: ArrayBuffer) => data;
224224
}
225225

226+
export function getBlob(
227+
service: FirebaseStorageImpl,
228+
location: Location
229+
): RequestInfo<Blob, Blob> {
230+
const urlPart = location.fullServerUrl();
231+
const url = makeUrl(urlPart, service.host) + '?alt=media';
232+
const method = 'GET';
233+
const timeout = service.maxOperationRetryTime;
234+
const requestInfo = new RequestInfo(url, method, getBlobHandler(), timeout);
235+
requestInfo.errorHandler = objectErrorHandler(location);
236+
return requestInfo;
237+
}
238+
239+
export function getBlobHandler(): RequestHandler<Blob, Blob> {
240+
return (xhr: Connection<Blob>, data: Blob) => data;
241+
}
242+
226243
export function getDownloadUrl(
227244
service: FirebaseStorageImpl,
228245
location: Location,

packages/storage/src/platform/browser/connection.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,54 @@ export function newBytesConnection(): Connection<ArrayBuffer> {
190190
return new XhrBytesConnection();
191191
}
192192

193+
const MAX_ERROR_MSG_LENGTH = 512;
194+
195+
export class XhrBlobConnection extends XhrConnection<Blob> {
196+
private data_?: Blob;
197+
private text_?: string;
198+
199+
initXhr(): void {
200+
// We use Blob here instead of ArrayBuffer to ensure that this code works
201+
// in Opera.
202+
this.xhr_.responseType = 'blob';
203+
}
204+
205+
getResponseText(): string {
206+
if (!this.sent_) {
207+
throw internalError('cannot .getResponseText() before sending');
208+
}
209+
return this.text_!;
210+
}
211+
212+
getResponse(): Blob {
213+
if (!this.sent_) {
214+
throw internalError('cannot .getResponse() before sending');
215+
}
216+
return this.data_!;
217+
}
218+
219+
send(
220+
url: string,
221+
method: string,
222+
body?: ArrayBufferView | Blob | string,
223+
headers?: Headers
224+
): Promise<void> {
225+
return super
226+
.send(url, method, body, headers)
227+
.then(() => {
228+
this.data_ = this.xhr_.response;
229+
return this.data_!.slice(0, MAX_ERROR_MSG_LENGTH, 'string').text();
230+
})
231+
.then(text => {
232+
this.text_ = text;
233+
});
234+
}
235+
}
236+
237+
export function newBlobConnection(): Connection<Blob> {
238+
return new XhrBlobConnection();
239+
}
240+
193241
export function injectTestConnection(
194242
factory: (() => Connection<string>) | null
195243
): void {

packages/storage/src/platform/connection.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Connection } from '../implementation/connection';
1818
import {
1919
newTextConnection as nodeNewTextConnection,
2020
newBytesConnection as nodeNewBytesConnection,
21+
newBlobConnection as nodeNewBlobConnection,
2122
injectTestConnection as nodeInjectTestConnection
2223
} from './node/connection';
2324

@@ -37,3 +38,8 @@ export function newBytesConnection(): Connection<ArrayBuffer> {
3738
// This file is only used under ts-node.
3839
return nodeNewBytesConnection();
3940
}
41+
42+
export function newBlobConnection(): Connection<Blob> {
43+
// This file is only used under ts-node.
44+
return nodeNewBlobConnection();
45+
}

packages/storage/src/platform/node/connection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,7 @@ export function injectTestConnection(
151151
): void {
152152
textFactoryOverride = factory;
153153
}
154+
155+
export function newBlobConnection(): Connection<Blob> {
156+
throw new Error('Blobs are not supported on Node');
157+
}

packages/storage/src/reference.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
getDownloadUrl as requestsGetDownloadUrl,
3131
deleteObject as requestsDeleteObject,
3232
multipartUpload,
33-
getBytes
33+
getBytes,
34+
getBlob
3435
} from './implementation/requests';
3536
import { ListOptions } from '../exp/public-types';
3637
import { StringFormat, dataFromString } from './implementation/string';
@@ -41,7 +42,11 @@ import { UploadTask } from './task';
4142
import { invalidRootOperation, noDownloadURL } from './implementation/error';
4243
import { validateNumber } from './implementation/type';
4344
import { UploadResult } from './tasksnapshot';
44-
import { newBytesConnection, newTextConnection } from './platform/connection';
45+
import {
46+
newBytesConnection,
47+
newTextConnection,
48+
newBlobConnection
49+
} from './platform/connection';
4550

4651
/**
4752
* Provides methods to interact with a bucket in the Firebase Storage service.
@@ -157,6 +162,18 @@ export function getBytesInternal(ref: Reference): Promise<ArrayBuffer> {
157162
.then(request => request.getPromise());
158163
}
159164

165+
/**
166+
* Download the bytes at the object's location.
167+
* @returns A Promise containing the downloaded bytes.
168+
*/
169+
export function getBlobInternal(ref: Reference): Promise<Blob> {
170+
ref._throwIfRoot('getBytes');
171+
const requestInfo = getBlob(ref.storage, ref._location);
172+
return ref.storage
173+
.makeRequestWithTokens(requestInfo, newBlobConnection)
174+
.then(request => request.getPromise());
175+
}
176+
160177
/**
161178
* Uploads data to this object's location.
162179
* The upload is not resumable.

packages/storage/test/browser/blob.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,32 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { assert } from 'chai';
18+
import { assert, expect } from 'chai';
1919
import * as sinon from 'sinon';
2020
import { FbsBlob } from '../../src/implementation/blob';
2121
import * as type from '../../src/implementation/type';
2222
import * as testShared from '../unit/testshared';
23+
import { ref, uploadBytes } from '../../exp/api';
24+
import * as types from '../../exp/public-types';
25+
import { createApp, createStorage } from '../integration/integration.exp.test';
26+
// eslint-disable-next-line import/no-extraneous-dependencies
27+
import { FirebaseApp, deleteApp } from '@firebase/app-exp';
28+
// eslint-disable-next-line import/no-extraneous-dependencies
29+
import { getBlob } from '../../exp/api.browser';
2330

2431
describe('Firebase Storage > Blob', () => {
32+
let app: FirebaseApp;
33+
let storage: types.FirebaseStorage;
34+
35+
beforeEach(async () => {
36+
app = await createApp();
37+
storage = createStorage(app);
38+
});
39+
40+
afterEach(async () => {
41+
await deleteApp(app);
42+
});
43+
2544
let stubs: sinon.SinonStub[] = [];
2645
before(() => {
2746
const definedStub = sinon.stub(type, 'isNativeBlobDefined');
@@ -47,6 +66,7 @@ describe('Firebase Storage > Blob', () => {
4766
new Uint8Array([2, 3, 4, 5])
4867
);
4968
});
69+
5070
it('Blobs are merged with strings correctly', () => {
5171
const blob = new FbsBlob(new Uint8Array([1, 2, 3, 4]));
5272
const merged = FbsBlob.getBlob('what', blob, '\ud83d\ude0a ')!;
@@ -70,4 +90,26 @@ describe('Firebase Storage > Blob', () => {
7090

7191
assert.equal(20, concatenated!.size());
7292
});
93+
94+
it('can get blob', async () => {
95+
const reference = ref(storage, 'public/exp-bytes');
96+
await uploadBytes(reference, new Uint8Array([0, 1, 3, 128, 255]));
97+
const blob = await getBlob(reference);
98+
const bytes = await blob.arrayBuffer();
99+
expect(new Uint8Array(bytes)).to.deep.equal(
100+
new Uint8Array([0, 1, 3, 128, 255])
101+
);
102+
});
103+
104+
it('getBlob() throws for missing file', async () => {
105+
const reference = ref(storage, 'public/exp-bytes-missing');
106+
try {
107+
await getBlob(reference);
108+
expect.fail();
109+
} catch (e) {
110+
expect(e.message).to.satisfy((v: string) =>
111+
v.match(/Object 'public\/exp-bytes-missing' does not exist/)
112+
);
113+
}
114+
});
73115
});

packages/storage/test/integration/integration.exp.test.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,28 @@ export const STORAGE_BUCKET = PROJECT_CONFIG.storageBucket;
4747
export const API_KEY = PROJECT_CONFIG.apiKey;
4848
export const AUTH_DOMAIN = PROJECT_CONFIG.authDomain;
4949

50+
export async function createApp(): Promise<FirebaseApp> {
51+
const app = initializeApp({
52+
apiKey: API_KEY,
53+
projectId: PROJECT_ID,
54+
storageBucket: STORAGE_BUCKET,
55+
authDomain: AUTH_DOMAIN
56+
});
57+
await signInAnonymously(getAuth(app));
58+
return app;
59+
}
60+
61+
export function createStorage(app: FirebaseApp): types.FirebaseStorage {
62+
return getStorage(app);
63+
}
64+
5065
describe('FirebaseStorage Exp', () => {
5166
let app: FirebaseApp;
5267
let storage: types.FirebaseStorage;
5368

5469
beforeEach(async () => {
55-
app = initializeApp({
56-
apiKey: API_KEY,
57-
projectId: PROJECT_ID,
58-
storageBucket: STORAGE_BUCKET,
59-
authDomain: AUTH_DOMAIN
60-
});
61-
await signInAnonymously(getAuth(app));
62-
storage = getStorage(app);
70+
app = await createApp();
71+
storage = createStorage(app);
6372
});
6473

6574
afterEach(async () => {

0 commit comments

Comments
 (0)