Page MenuHomePhabricator

D7649.id26180.diff
No OneTemporary

D7649.id26180.diff

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

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)

Event Timeline