Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 0 additions & 7 deletions .github/scripts/compare-types/packages/storage/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,6 @@ const config: PackageConfig = {
'(the Web Streams API type). The Node.js stream type is used because the ' +
'React Native environment does not have the Web Streams API.',
},
{
name: 'uploadBytes',
reason:
'Returns `Promise<TaskResult>` in RN Firebase instead of `Promise<UploadResult>`. ' +
'`TaskResult` is a type alias for `UploadResult`, so the runtime shape is identical; ' +
'the different name is for consistency with the native task system.',
},
{
name: 'uploadBytesResumable',
reason:
Expand Down
38 changes: 38 additions & 0 deletions packages/storage/e2e/StorageTask.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,44 @@ describe('storage() -> StorageTask', function () {
});
});

describe('uploadBytes()', function () {
it('resolves with ref and metadata whose size matches uploaded bytes', async function () {
const { getStorage, ref, uploadBytes } = storageModular;
const jsonDerulo = JSON.stringify({ foo: 'bar' });
const expectedByteLength = jsonDerulo.length;

const jsonDeruloBytes = new Uint8Array([...jsonDerulo].map(char => char.charCodeAt(0)));

const uploadResult = await uploadBytes(
ref(getStorage(), `${PATH}/uploadBytesModular.json`),
jsonDeruloBytes,
{
contentType: 'application/json',
},
);

uploadResult.ref.fullPath.should.containEql('uploadBytesModular.json');
uploadResult.metadata.should.be.an.Object();
uploadResult.metadata.size.should.eql(expectedByteLength);
uploadResult.metadata.contentType.should.eql('application/json');
});

it('rejects when metadata is not an object', async function () {
const { getStorage, ref, uploadBytes } = storageModular;
try {
await uploadBytes(
ref(getStorage(), `${PATH}/uploadBytesBadMeta.json`),
new ArrayBuffer(),
123,
);
return Promise.reject(new Error('Did not error!'));
} catch (error) {
error.message.should.containEql('must be an object value');
return Promise.resolve();
}
});
});

describe('upload tasks', function () {
// before(async function () {
// // TODO we need some semi-large assets to upload and download I think?
Expand Down
34 changes: 34 additions & 0 deletions packages/storage/lib/StorageReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ import type {
} from './types/storage';
import type { ListResultInternal, StorageInternal } from './types/internal';

async function putPayloadToUint8Array(data: Blob | Uint8Array | ArrayBuffer): Promise<Uint8Array> {
if (data instanceof Uint8Array) {
return data.byteOffset === 0 && data.byteLength === data.buffer.byteLength
? data
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
}
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
return new Uint8Array(
await new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(data);
}),
);
}

export default class Reference extends ReferenceBase implements StorageReference {
_storage: StorageInternal;

Expand Down Expand Up @@ -206,6 +225,21 @@ export default class Reference extends ReferenceBase implements StorageReference
put(data: Blob | Uint8Array | ArrayBuffer, metadata?: UploadMetadata): Task {
const validatedMetadata = isUndefined(metadata) ? metadata : validateMetadata(metadata, false);

// Firebase-js fallback (e.g. macOS) exposes `uploadBinary`; iOS/Android native omit it.
const uploadBinary = this._storage.native.uploadBinary;
if (uploadBinary) {
return new StorageUploadTask(this, async task => {
const bytes = await putPayloadToUint8Array(data);
return uploadBinary.call(
this._storage.native,
this.toString(),
bytes,
validatedMetadata,
task._id,
);
});
}

return new StorageUploadTask(this, task =>
Base64.fromData(data).then(({ string, format }) => {
const { _string, _format, _metadata } = this._updateString(
Expand Down
76 changes: 65 additions & 11 deletions packages/storage/lib/modular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@

import { getApp } from '@react-native-firebase/app';
import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/dist/module/common';
import { NativeFirebaseError } from '@react-native-firebase/app/dist/module/internal';
import type { FirebaseApp } from '@react-native-firebase/app';
import type {
FirebaseStorage,
StorageReference,
FullMetadata,
ListResult,
ListOptions,
TaskResult,
Task,
SettableMetadata,
UploadMetadata,
EmulatorMockTokenOptions,
UploadResult,
} from './types/storage';
import { TaskEvent, TaskState } from './types/storage';
import type { StorageReferenceInternal, StorageInternal } from './types/internal';

type WithModularDeprecationArg<F> = F extends (...args: infer P) => infer R
Expand Down Expand Up @@ -267,18 +269,70 @@ export function updateMetadata(
}

/**
* Uploads data to this object's location. The upload is not resumable.
* @param _storageRef - Storage `Reference` instance.
* @param _data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location.
* @param _metadata - A Storage `UploadMetadata` instance to update. Optional.
* @returns {Promise<TaskResult>}
* Uploads data to this object's location. The upload is not resumable. If the upload is canceled,
* the Promise rejects with a {@link NativeFirebaseError} (typically `storage/cancelled`),
* matching other storage upload tasks. Other failures reject with the same error type as {@link uploadBytesResumable}.
* @param storageRef - Storage `Reference` instance.
* @param data - The data (Blob | Uint8Array | ArrayBuffer) to upload to the storage bucket at the reference location.
* @param metadata - A Storage `UploadMetadata` instance to update. Optional.
* @returns {Promise<UploadResult>}
*/
export async function uploadBytes(
_storageRef: StorageReference,
_data: Blob | Uint8Array | ArrayBuffer,
_metadata?: UploadMetadata,
): Promise<TaskResult> {
throw new Error('`uploadBytes()` is not implemented');
storageRef: StorageReference,
data: Blob | Uint8Array | ArrayBuffer,
metadata?: UploadMetadata,
): Promise<UploadResult> {
const task = uploadBytesResumable(storageRef, data, metadata);
return new Promise((resolve, reject) => {
let completed = false;
const subscription: { unsubscribe?: () => void } = {};

const settle = (fn: () => void) => {
if (completed) {
return;
}
completed = true;
subscription.unsubscribe?.();
fn();
};

subscription.unsubscribe = task.on(
TaskEvent.STATE_CHANGED,
taskSnapshot => {
switch (taskSnapshot.state) {
case TaskState.RUNNING:
break;
case TaskState.PAUSED:
// we are wrapping the resumable version, just resume if it pauses
task.resume();
break;
case TaskState.SUCCESS:
settle(() => resolve({ ref: taskSnapshot.ref, metadata: taskSnapshot.metadata }));
break;
case TaskState.CANCELED:
settle(() =>
reject(
NativeFirebaseError.fromEvent(
{ code: 'cancelled', message: 'User cancelled the operation.' },
'storage',
),
),
);
break;
case TaskState.ERROR:
// this is handled in the dedicated error listener below
break;
default:
settle(() =>
reject(new Error(`Unhandled task state in uploadBytes: ${taskSnapshot.state}`)),
);
}
},
error => {
settle(() => reject(error));
},
);
});
Comment thread
mikehardy marked this conversation as resolved.
}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/storage/lib/types/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,22 @@ export interface RNFBStorageModule {
metadata: UploadMetadata | undefined,
taskId: number,
): Promise<TaskSnapshot>;

/** Optional on firebase-js fallback only; native modules use base64 + {@link putString}. */
uploadBinary?(
url: string,
data: Uint8Array,
metadata: UploadMetadata | undefined,
taskId: number,
): Promise<TaskSnapshot>;

putFile(
url: string,
filePath: string,
metadata: UploadMetadata | undefined,
taskId: number,
): Promise<TaskSnapshot>;

writeToFile(url: string, filePath: string, taskId: number): Promise<TaskSnapshot>;

/**
Expand Down
Loading
Loading