Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3690414
D7649.id26180.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
D7649.id26180.diff
View Options
diff --git a/lib/utils/base64.js b/lib/utils/base64.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/base64.js
@@ -0,0 +1,18 @@
+// @flow
+
+/**
+ * Converts a base64 string to base64url format specified in RFC 4648 § 5.
+ */
+function toBase64Url(
+ base64String: string,
+ stripPadding: boolean = true,
+): string {
+ const base64Url = base64String.replace(/\+/g, '-').replace(/\//g, '_');
+
+ if (!stripPadding) {
+ return base64Url;
+ }
+ return base64Url.replace(/=/g, '');
+}
+
+export { toBase64Url };
diff --git a/lib/utils/base64.test.js b/lib/utils/base64.test.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/base64.test.js
@@ -0,0 +1,18 @@
+// @flow
+
+import { toBase64Url } from './base64.js';
+
+describe('toBase64Url', () => {
+ it('converts base64 to base64url', () => {
+ expect(toBase64Url('aGVsbG8gd29ybGQ=')).toStrictEqual('aGVsbG8gd29ybGQ');
+ expect(
+ toBase64Url('qL8R4QIcQ/ZsRqOAbeRfcZhilN/MksRtDaErMA=='),
+ ).toStrictEqual('qL8R4QIcQ_ZsRqOAbeRfcZhilN_MksRtDaErMA');
+ });
+
+ it('does not strip padding if stripPadding is false', () => {
+ expect(toBase64Url('aGVsbG8gd29ybGQ=', false)).toStrictEqual(
+ 'aGVsbG8gd29ybGQ=',
+ );
+ });
+});
diff --git a/native/input/input-state-container.react.js b/native/input/input-state-container.react.js
--- a/native/input/input-state-container.react.js
+++ b/native/input/input-state-container.react.js
@@ -6,6 +6,7 @@
import * as Upload from 'react-native-background-upload';
import { useDispatch } from 'react-redux';
import { createSelector } from 'reselect';
+import * as uuid from 'uuid';
import {
createLocalMessageActionType,
@@ -18,10 +19,12 @@
import { newThread } from 'lib/actions/thread-actions.js';
import {
uploadMultimedia,
+ uploadMediaMetadata,
updateMultimediaMessageMediaActionType,
type MultimediaUploadCallbacks,
type MultimediaUploadExtras,
} from 'lib/actions/upload-actions.js';
+import blobService from 'lib/facts/blob-service.js';
import commStaffCommunity from 'lib/facts/comm-staff-community.js';
import { pathFromURI, replaceExtension } from 'lib/media/file-utils.js';
import {
@@ -48,11 +51,13 @@
} from 'lib/shared/thread-utils.js';
import type { CalendarQuery } from 'lib/types/entry-types.js';
import type {
+ Dimensions,
UploadMultimediaResult,
Media,
NativeMediaSelection,
MediaMissionResult,
MediaMission,
+ UploadMediaMetadataRequest,
} from 'lib/types/media-types.js';
import { messageTypes } from 'lib/types/message-types-enum.js';
import {
@@ -85,6 +90,8 @@
useServerCall,
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
+import { toBase64Url } from 'lib/utils/base64.js';
+import { makeBlobServiceEndpointURL } from 'lib/utils/blob-service.js';
import type {
CallServerEndpointOptions,
CallServerEndpointResponse,
@@ -138,6 +145,9 @@
extras: MultimediaUploadExtras,
callbacks: MultimediaUploadCallbacks,
) => Promise<UploadMultimediaResult>,
+ +uploadMediaMetadata: (
+ input: UploadMediaMetadataRequest,
+ ) => Promise<UploadMultimediaResult>,
+sendMultimediaMessage: (
threadID: string,
localID: string,
@@ -1072,6 +1082,110 @@
});
}
+ async blobServiceUpload(
+ input: {
+ uri: string,
+ filename: string,
+ mimeType: string,
+ blobHash: string,
+ encryptionKey: string,
+ dimensions: Dimensions,
+ loop?: boolean,
+ },
+ options?: ?CallServerEndpointOptions,
+ ): Promise<void> {
+ const newHolder = uuid.v4();
+ const blobHash = toBase64Url(input.blobHash);
+
+ // 1. Assign new holder for blob with given blobHash
+ let blobAlreadyExists: boolean;
+ try {
+ const assignHolderEndpoint = blobService.httpEndpoints.ASSIGN_HOLDER;
+ const assignHolderResponse = await fetch(
+ makeBlobServiceEndpointURL(assignHolderEndpoint),
+ {
+ method: assignHolderEndpoint.method,
+ body: JSON.stringify({
+ holder: newHolder,
+ blob_hash: blobHash,
+ }),
+ headers: {
+ 'content-type': 'application/json',
+ },
+ },
+ );
+
+ if (!assignHolderResponse.ok) {
+ const { status, statusText } = assignHolderResponse;
+ throw new Error(`Server responded with HTTP ${status}: ${statusText}`);
+ }
+ const { data_exists: dataExistsResponse } =
+ await assignHolderResponse.json();
+ blobAlreadyExists = dataExistsResponse;
+ } catch (e) {
+ throw new Error(
+ `Failed to assign holder: ${
+ getMessageForException(e) ?? 'unknown error'
+ }`,
+ );
+ }
+
+ // 2. Upload blob contents if blob doesn't exist
+ if (!blobAlreadyExists) {
+ let path = input.uri;
+ if (Platform.OS === 'android') {
+ const resolvedPath = pathFromURI(input.uri);
+ if (resolvedPath) {
+ path = resolvedPath;
+ }
+ }
+ const uploadEndpoint = blobService.httpEndpoints.UPLOAD_BLOB;
+ const { method } = uploadEndpoint;
+ const uploadID = await Upload.startUpload({
+ url: makeBlobServiceEndpointURL(uploadEndpoint),
+ method,
+ path,
+ type: 'multipart',
+ field: 'blob_data',
+ parameters: {
+ blob_hash: blobHash,
+ },
+ });
+ if (options && options.abortHandler) {
+ options.abortHandler(() => {
+ Upload.cancelUpload(uploadID);
+ });
+ }
+ await new Promise((resolve, reject) => {
+ Upload.addListener('error', uploadID, data => {
+ reject(data.error);
+ });
+ Upload.addListener('cancelled', uploadID, () => {
+ reject(new Error('request aborted'));
+ });
+ Upload.addListener('completed', uploadID, data => {
+ resolve(data);
+ });
+ if (options && options.onProgress) {
+ const { onProgress } = options;
+ Upload.addListener('progress', uploadID, data =>
+ onProgress(data.progress / 100),
+ );
+ }
+ });
+ }
+
+ // 3. Send upload metadata to the keyserver, return response
+ return await this.props.uploadMediaMetadata({
+ ...input.dimensions,
+ loop: input.loop ?? false,
+ blobHolder: newHolder,
+ encryptionKey: input.encryptionKey,
+ mimeType: input.mimeType,
+ filename: input.filename,
+ });
+ }
+
uploadBlob = async (
url: string,
cookie: ?string,
@@ -1622,6 +1736,7 @@
const hasWiFi = useSelector(state => state.connectivity.hasWiFi);
const calendarQuery = useCalendarQuery();
const callUploadMultimedia = useServerCall(uploadMultimedia);
+ const callUploadMediaMetadata = useServerCall(uploadMediaMetadata);
const callSendMultimediaMessage = useServerCall(sendMultimediaMessage);
const callSendTextMessage = useServerCall(sendTextMessage);
const callNewThread = useServerCall(newThread);
@@ -1635,6 +1750,7 @@
return (
<InputStateContainer
{...props}
+ uploadMediaMetadata={callUploadMediaMetadata}
viewerID={viewerID}
nextLocalID={nextLocalID}
messageStoreMessages={messageStoreMessages}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Jan 8, 4:52 AM (14 m, 23 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2813473
Default Alt Text
D7649.id26180.diff (7 KB)
Attached To
Mode
D7649: [native] Add function to upload media to blob service
Attached
Detach File
Event Timeline
Log In to Comment