@protontech/drive-sdk 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crypto/driveCrypto.d.ts +1 -0
- package/dist/crypto/driveCrypto.js +1 -0
- package/dist/crypto/driveCrypto.js.map +1 -1
- package/dist/interface/events.d.ts +1 -1
- package/dist/interface/featureFlags.d.ts +2 -1
- package/dist/interface/featureFlags.js +1 -0
- package/dist/interface/featureFlags.js.map +1 -1
- package/dist/interface/httpClient.d.ts +1 -0
- package/dist/interface/nodes.d.ts +7 -0
- package/dist/interface/nodes.js.map +1 -1
- package/dist/interface/telemetry.d.ts +4 -0
- package/dist/interface/telemetry.js.map +1 -1
- package/dist/internal/apiService/apiService.d.ts +1 -0
- package/dist/internal/apiService/apiService.js +16 -7
- package/dist/internal/apiService/apiService.js.map +1 -1
- package/dist/internal/apiService/apiService.test.js +24 -0
- package/dist/internal/apiService/apiService.test.js.map +1 -1
- package/dist/internal/apiService/driveTypes.d.ts +162 -101
- package/dist/internal/download/telemetry.js +4 -0
- package/dist/internal/download/telemetry.js.map +1 -1
- package/dist/internal/download/telemetry.test.js +5 -0
- package/dist/internal/download/telemetry.test.js.map +1 -1
- package/dist/internal/events/index.js +2 -2
- package/dist/internal/events/index.js.map +1 -1
- package/dist/internal/events/interface.d.ts +1 -1
- package/dist/internal/nodes/apiService.d.ts +4 -0
- package/dist/internal/nodes/apiService.js +4 -0
- package/dist/internal/nodes/apiService.js.map +1 -1
- package/dist/internal/nodes/apiService.test.js +8 -0
- package/dist/internal/nodes/apiService.test.js.map +1 -1
- package/dist/internal/nodes/cryptoService.js +3 -0
- package/dist/internal/nodes/cryptoService.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.d.ts +5 -5
- package/dist/internal/nodes/extendedAttributes.js +5 -14
- package/dist/internal/nodes/extendedAttributes.js.map +1 -1
- package/dist/internal/nodes/extendedAttributes.test.js +16 -22
- package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
- package/dist/internal/nodes/interface.d.ts +5 -0
- package/dist/internal/nodes/nodesManagement.d.ts +3 -3
- package/dist/internal/nodes/nodesManagement.js +7 -5
- package/dist/internal/nodes/nodesManagement.js.map +1 -1
- package/dist/internal/photos/albumsManager.js +1 -0
- package/dist/internal/photos/albumsManager.js.map +1 -1
- package/dist/internal/photos/nodes.d.ts +1 -1
- package/dist/internal/photos/nodes.js +2 -2
- package/dist/internal/photos/nodes.js.map +1 -1
- package/dist/internal/photos/upload.d.ts +5 -5
- package/dist/internal/photos/upload.js +8 -2
- package/dist/internal/photos/upload.js.map +1 -1
- package/dist/internal/sharing/apiService.js +1 -1
- package/dist/internal/sharing/apiService.js.map +1 -1
- package/dist/internal/sharingPublic/nodes.d.ts +1 -0
- package/dist/internal/upload/apiService.d.ts +45 -1
- package/dist/internal/upload/apiService.js +69 -1
- package/dist/internal/upload/apiService.js.map +1 -1
- package/dist/internal/upload/blockVerifier.d.ts +4 -1
- package/dist/internal/upload/blockVerifier.js +5 -0
- package/dist/internal/upload/blockVerifier.js.map +1 -1
- package/dist/internal/upload/cryptoService.d.ts +2 -2
- package/dist/internal/upload/cryptoService.js +1 -3
- package/dist/internal/upload/cryptoService.js.map +1 -1
- package/dist/internal/upload/fileUploader.d.ts +4 -3
- package/dist/internal/upload/fileUploader.js +17 -7
- package/dist/internal/upload/fileUploader.js.map +1 -1
- package/dist/internal/upload/index.d.ts +3 -3
- package/dist/internal/upload/index.js +17 -1
- package/dist/internal/upload/index.js.map +1 -1
- package/dist/internal/upload/index.test.d.ts +1 -0
- package/dist/internal/upload/index.test.js +71 -0
- package/dist/internal/upload/index.test.js.map +1 -0
- package/dist/internal/upload/interface.d.ts +2 -0
- package/dist/internal/upload/manager.d.ts +41 -2
- package/dist/internal/upload/manager.js +126 -44
- package/dist/internal/upload/manager.js.map +1 -1
- package/dist/internal/upload/manager.test.js +268 -1
- package/dist/internal/upload/manager.test.js.map +1 -1
- package/dist/internal/upload/smallFileUploader.d.ts +83 -0
- package/dist/internal/upload/smallFileUploader.js +197 -0
- package/dist/internal/upload/smallFileUploader.js.map +1 -0
- package/dist/internal/upload/smallFileUploader.test.d.ts +1 -0
- package/dist/internal/upload/smallFileUploader.test.js +358 -0
- package/dist/internal/upload/smallFileUploader.test.js.map +1 -0
- package/dist/internal/upload/streamReader.d.ts +4 -0
- package/dist/internal/upload/streamReader.js +37 -0
- package/dist/internal/upload/streamReader.js.map +1 -0
- package/dist/internal/upload/streamReader.test.d.ts +1 -0
- package/dist/internal/upload/streamReader.test.js +90 -0
- package/dist/internal/upload/streamReader.test.js.map +1 -0
- package/dist/internal/upload/streamUploader.d.ts +6 -0
- package/dist/internal/upload/streamUploader.js +3 -3
- package/dist/internal/upload/streamUploader.js.map +1 -1
- package/dist/internal/upload/telemetry.d.ts +3 -2
- package/dist/internal/upload/telemetry.js +5 -0
- package/dist/internal/upload/telemetry.js.map +1 -1
- package/dist/internal/upload/telemetry.test.js +6 -0
- package/dist/internal/upload/telemetry.test.js.map +1 -1
- package/dist/protonDrivePhotosClient.d.ts +1 -1
- package/dist/protonDrivePublicLinkClient.js +3 -1
- package/dist/protonDrivePublicLinkClient.js.map +1 -1
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.js +21 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.d.ts +1 -0
- package/dist/telemetry.test.js +37 -0
- package/dist/telemetry.test.js.map +1 -0
- package/dist/transformers.d.ts +1 -1
- package/dist/transformers.js +1 -0
- package/dist/transformers.js.map +1 -1
- package/package.json +1 -1
- package/src/crypto/driveCrypto.ts +2 -0
- package/src/interface/events.ts +1 -1
- package/src/interface/featureFlags.ts +1 -0
- package/src/interface/httpClient.ts +1 -0
- package/src/interface/nodes.ts +7 -0
- package/src/interface/telemetry.ts +4 -0
- package/src/internal/apiService/apiService.test.ts +30 -0
- package/src/internal/apiService/apiService.ts +23 -7
- package/src/internal/apiService/driveTypes.ts +162 -101
- package/src/internal/download/telemetry.test.ts +5 -0
- package/src/internal/download/telemetry.ts +5 -1
- package/src/internal/events/index.ts +2 -2
- package/src/internal/events/interface.ts +1 -1
- package/src/internal/nodes/apiService.test.ts +9 -0
- package/src/internal/nodes/apiService.ts +4 -0
- package/src/internal/nodes/cryptoService.ts +11 -1
- package/src/internal/nodes/extendedAttributes.test.ts +25 -25
- package/src/internal/nodes/extendedAttributes.ts +10 -19
- package/src/internal/nodes/interface.ts +5 -0
- package/src/internal/nodes/nodesManagement.ts +8 -6
- package/src/internal/photos/albumsManager.ts +1 -0
- package/src/internal/photos/nodes.ts +2 -2
- package/src/internal/photos/upload.ts +23 -10
- package/src/internal/sharing/apiService.ts +5 -5
- package/src/internal/upload/apiService.ts +167 -2
- package/src/internal/upload/blockVerifier.ts +12 -0
- package/src/internal/upload/cryptoService.ts +10 -10
- package/src/internal/upload/fileUploader.ts +20 -7
- package/src/internal/upload/index.test.ts +99 -0
- package/src/internal/upload/index.ts +45 -4
- package/src/internal/upload/interface.ts +2 -0
- package/src/internal/upload/manager.test.ts +368 -2
- package/src/internal/upload/manager.ts +229 -78
- package/src/internal/upload/smallFileUploader.test.ts +491 -0
- package/src/internal/upload/smallFileUploader.ts +353 -0
- package/src/internal/upload/streamReader.test.ts +109 -0
- package/src/internal/upload/streamReader.ts +38 -0
- package/src/internal/upload/streamUploader.ts +1 -1
- package/src/internal/upload/telemetry.test.ts +6 -0
- package/src/internal/upload/telemetry.ts +8 -2
- package/src/protonDrivePhotosClient.ts +1 -1
- package/src/protonDrivePublicLinkClient.ts +2 -0
- package/src/telemetry.test.ts +40 -0
- package/src/telemetry.ts +22 -0
- package/src/transformers.ts +2 -0
|
@@ -312,6 +312,7 @@ export abstract class NodesManagementBase<
|
|
|
312
312
|
async createFolder(parentNodeUid: string, folderName: string, modificationTime?: Date): Promise<TDecryptedNode> {
|
|
313
313
|
validateNodeName(folderName);
|
|
314
314
|
|
|
315
|
+
const parentNode = await this.nodesAccess.getNode(parentNodeUid);
|
|
315
316
|
const parentKeys = await this.nodesAccess.getNodeKeys(parentNodeUid);
|
|
316
317
|
if (!parentKeys.hashKey) {
|
|
317
318
|
throw new ValidationError(c('Error').t`Creating folders in non-folders is not allowed`);
|
|
@@ -338,14 +339,14 @@ export abstract class NodesManagementBase<
|
|
|
338
339
|
});
|
|
339
340
|
|
|
340
341
|
await this.nodesAccess.notifyChildCreated(parentNodeUid);
|
|
341
|
-
const node = this.generateNodeFolder(
|
|
342
|
+
const node = this.generateNodeFolder(parentNode, nodeUid, folderName, encryptedCrypto);
|
|
342
343
|
await this.cryptoCache.setNodeKeys(nodeUid, keys);
|
|
343
344
|
return node;
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
protected abstract generateNodeFolder(
|
|
348
|
+
parentNode: TDecryptedNode,
|
|
347
349
|
nodeUid: string,
|
|
348
|
-
parentUid: string,
|
|
349
350
|
name: string,
|
|
350
351
|
encryptedCrypto: {
|
|
351
352
|
hash: string;
|
|
@@ -355,8 +356,8 @@ export abstract class NodesManagementBase<
|
|
|
355
356
|
): TDecryptedNode;
|
|
356
357
|
|
|
357
358
|
protected generateNodeFolderBase(
|
|
359
|
+
parentNode: TDecryptedNode,
|
|
358
360
|
nodeUid: string,
|
|
359
|
-
parentNodeUid: string,
|
|
360
361
|
name: string,
|
|
361
362
|
encryptedCrypto: {
|
|
362
363
|
hash: string;
|
|
@@ -371,7 +372,7 @@ export abstract class NodesManagementBase<
|
|
|
371
372
|
|
|
372
373
|
// Basic node metadata
|
|
373
374
|
uid: nodeUid,
|
|
374
|
-
parentUid:
|
|
375
|
+
parentUid: parentNode.uid,
|
|
375
376
|
type: NodeType.Folder,
|
|
376
377
|
mediaType: FOLDER_MEDIA_TYPE,
|
|
377
378
|
creationTime: new Date(),
|
|
@@ -381,6 +382,7 @@ export abstract class NodesManagementBase<
|
|
|
381
382
|
isShared: false,
|
|
382
383
|
isSharedPublicly: false,
|
|
383
384
|
directRole: MemberRole.Inherited,
|
|
385
|
+
ownedBy: parentNode.ownedBy,
|
|
384
386
|
|
|
385
387
|
// Decrypted metadata
|
|
386
388
|
isStale: false,
|
|
@@ -432,8 +434,8 @@ export abstract class NodesManagementBase<
|
|
|
432
434
|
|
|
433
435
|
export class NodesManagement extends NodesManagementBase {
|
|
434
436
|
protected generateNodeFolder(
|
|
437
|
+
parentNode: DecryptedNode,
|
|
435
438
|
nodeUid: string,
|
|
436
|
-
parentNodeUid: string,
|
|
437
439
|
name: string,
|
|
438
440
|
encryptedCrypto: {
|
|
439
441
|
hash: string;
|
|
@@ -441,6 +443,6 @@ export class NodesManagement extends NodesManagementBase {
|
|
|
441
443
|
signatureEmail: string | null;
|
|
442
444
|
},
|
|
443
445
|
): DecryptedNode {
|
|
444
|
-
return this.generateNodeFolderBase(
|
|
446
|
+
return this.generateNodeFolderBase(parentNode, nodeUid, name, encryptedCrypto);
|
|
445
447
|
}
|
|
446
448
|
}
|
|
@@ -252,8 +252,8 @@ export class PhotosNodesManagement extends NodesManagementBase<
|
|
|
252
252
|
PhotosNodesCryptoService
|
|
253
253
|
> {
|
|
254
254
|
protected generateNodeFolder(
|
|
255
|
+
parentNode: DecryptedPhotoNode,
|
|
255
256
|
nodeUid: string,
|
|
256
|
-
parentNodeUid: string,
|
|
257
257
|
name: string,
|
|
258
258
|
encryptedCrypto: {
|
|
259
259
|
hash: string;
|
|
@@ -261,6 +261,6 @@ export class PhotosNodesManagement extends NodesManagementBase<
|
|
|
261
261
|
signatureEmail: string | null;
|
|
262
262
|
},
|
|
263
263
|
): DecryptedPhotoNode {
|
|
264
|
-
return this.generateNodeFolderBase(
|
|
264
|
+
return this.generateNodeFolderBase(parentNode, nodeUid, name, encryptedCrypto);
|
|
265
265
|
}
|
|
266
266
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { DriveCrypto } from '../../crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ProtonDriveTelemetry,
|
|
4
|
+
UploadMetadata,
|
|
5
|
+
Thumbnail,
|
|
6
|
+
AnonymousUser,
|
|
7
|
+
FeatureFlagProvider,
|
|
8
|
+
PhotoTag,
|
|
9
|
+
} from '../../interface';
|
|
3
10
|
import { DriveAPIService, drivePaths } from '../apiService';
|
|
4
11
|
import { generateFileExtendedAttributes } from '../nodes';
|
|
5
|
-
import { splitNodeRevisionUid } from '../uids';
|
|
12
|
+
import { splitNodeRevisionUid, splitNodeUid } from '../uids';
|
|
6
13
|
import { UploadAPIService } from '../upload/apiService';
|
|
7
14
|
import { BlockVerifier } from '../upload/blockVerifier';
|
|
8
15
|
import { UploadController } from '../upload/controller';
|
|
@@ -22,9 +29,8 @@ type PostCommitRevisionResponse =
|
|
|
22
29
|
|
|
23
30
|
export type PhotoUploadMetadata = UploadMetadata & {
|
|
24
31
|
captureTime?: Date;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
|
|
32
|
+
mainPhotoNodeUid?: string;
|
|
33
|
+
tags?: PhotoTag[];
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
export class PhotoFileUploader extends FileUploader {
|
|
@@ -180,7 +186,7 @@ export class PhotoUploadManager extends UploadManager {
|
|
|
180
186
|
const photo = {
|
|
181
187
|
contentHash,
|
|
182
188
|
captureTime: uploadMetadata.captureTime || extendedAttributes.modificationTime,
|
|
183
|
-
|
|
189
|
+
mainPhotoNodeUid: uploadMetadata.mainPhotoNodeUid,
|
|
184
190
|
tags: uploadMetadata.tags,
|
|
185
191
|
};
|
|
186
192
|
await this.photoApiService.commitDraftPhoto(nodeRevisionDraft.nodeRevisionUid, nodeCommitCrypto, photo);
|
|
@@ -218,12 +224,19 @@ export class PhotoUploadAPIService extends UploadAPIService {
|
|
|
218
224
|
photo: {
|
|
219
225
|
contentHash: string;
|
|
220
226
|
captureTime?: Date;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
tags?: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
|
|
227
|
+
mainPhotoNodeUid?: string;
|
|
228
|
+
tags?: PhotoTag[];
|
|
224
229
|
},
|
|
225
230
|
): Promise<void> {
|
|
226
231
|
const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
|
|
232
|
+
const { volumeId: mainPhotoVolumeId, nodeId: mainPhotoNodeId } = photo.mainPhotoNodeUid
|
|
233
|
+
? splitNodeUid(photo.mainPhotoNodeUid)
|
|
234
|
+
: { volumeId: null, nodeId: null };
|
|
235
|
+
|
|
236
|
+
if (mainPhotoVolumeId !== null && mainPhotoVolumeId !== volumeId) {
|
|
237
|
+
throw new Error('mainPhotoNodeUid must belong to the same volume as the draft');
|
|
238
|
+
}
|
|
239
|
+
|
|
227
240
|
await this.apiService.put<
|
|
228
241
|
// TODO: Deprected fields but not properly marked in the types.
|
|
229
242
|
Omit<PostCommitRevisionRequest, 'BlockNumber' | 'BlockList' | 'ThumbnailToken' | 'State'>,
|
|
@@ -235,7 +248,7 @@ export class PhotoUploadAPIService extends UploadAPIService {
|
|
|
235
248
|
Photo: {
|
|
236
249
|
ContentHash: photo.contentHash,
|
|
237
250
|
CaptureTime: photo.captureTime ? Math.floor(photo.captureTime?.getTime() / 1000) : 0,
|
|
238
|
-
MainPhotoLinkID:
|
|
251
|
+
MainPhotoLinkID: mainPhotoNodeId,
|
|
239
252
|
Tags: photo.tags || [],
|
|
240
253
|
Exif: null, // Deprecated field, not used.
|
|
241
254
|
},
|
|
@@ -79,12 +79,12 @@ type PostCreateShareResponse =
|
|
|
79
79
|
drivePaths['/drive/volumes/{volumeID}/shares']['post']['responses']['200']['content']['application/json'];
|
|
80
80
|
|
|
81
81
|
type PostChangeSharePropertiesRequest = Extract<
|
|
82
|
-
drivePaths['/drive/shares/{shareID}/
|
|
82
|
+
drivePaths['/drive/shares/{shareID}/editors-can-share']['put']['requestBody'],
|
|
83
83
|
{ content: object }
|
|
84
84
|
>['content']['application/json'];
|
|
85
85
|
|
|
86
86
|
type PostChangeSharePropertiesResponse =
|
|
87
|
-
drivePaths['/drive/shares/{shareID}/
|
|
87
|
+
drivePaths['/drive/shares/{shareID}/editors-can-share']['put']['responses']['200']['content']['application/json'];
|
|
88
88
|
|
|
89
89
|
type PostInviteProtonUserRequest = Extract<
|
|
90
90
|
drivePaths['/drive/v2/shares/{shareID}/invitations']['post']['requestBody'],
|
|
@@ -423,9 +423,9 @@ export class SharingAPIService {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
async changeShareProperties(shareId: string, { editorsCanShare }: { editorsCanShare: boolean }) {
|
|
426
|
-
await this.apiService.
|
|
427
|
-
`drive/shares/${shareId}/
|
|
428
|
-
{
|
|
426
|
+
await this.apiService.put<PostChangeSharePropertiesRequest, PostChangeSharePropertiesResponse>(
|
|
427
|
+
`drive/shares/${shareId}/editors-can-share`,
|
|
428
|
+
{ Value: editorsCanShare },
|
|
429
429
|
);
|
|
430
430
|
}
|
|
431
431
|
|
|
@@ -52,6 +52,26 @@ type PostLoadLinksMetadataRequest = Extract<
|
|
|
52
52
|
type PostLoadLinksMetadataResponse =
|
|
53
53
|
drivePaths['/drive/v2/volumes/{volumeID}/links']['post']['responses']['200']['content']['application/json'];
|
|
54
54
|
|
|
55
|
+
type PostSmallFileFormData = Extract<
|
|
56
|
+
Extract<
|
|
57
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/small']['post']['requestBody'],
|
|
58
|
+
{ content: object }
|
|
59
|
+
>['content']['multipart/form-data'],
|
|
60
|
+
{ Metadata: object }
|
|
61
|
+
>;
|
|
62
|
+
type PostSmallFileResponse =
|
|
63
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/small']['post']['responses']['200']['content']['application/json'];
|
|
64
|
+
|
|
65
|
+
type PostSmallRevisionFormData = Extract<
|
|
66
|
+
Extract<
|
|
67
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/small']['post']['requestBody'],
|
|
68
|
+
{ content: object }
|
|
69
|
+
>['content']['multipart/form-data'],
|
|
70
|
+
{ Metadata: object }
|
|
71
|
+
>;
|
|
72
|
+
type PostSmallRevisionResponse =
|
|
73
|
+
drivePaths['/drive/v2/volumes/{volumeID}/files/{linkID}/revisions/small']['post']['responses']['200']['content']['application/json'];
|
|
74
|
+
|
|
55
75
|
export class UploadAPIService {
|
|
56
76
|
constructor(
|
|
57
77
|
protected apiService: DriveAPIService,
|
|
@@ -218,7 +238,7 @@ export class UploadAPIService {
|
|
|
218
238
|
options: {
|
|
219
239
|
armoredManifestSignature: string;
|
|
220
240
|
signatureEmail: string | AnonymousUser;
|
|
221
|
-
armoredExtendedAttributes
|
|
241
|
+
armoredExtendedAttributes: string;
|
|
222
242
|
},
|
|
223
243
|
): Promise<void> {
|
|
224
244
|
const { volumeId, nodeId, revisionId } = splitNodeRevisionUid(draftNodeRevisionUid);
|
|
@@ -229,7 +249,7 @@ export class UploadAPIService {
|
|
|
229
249
|
>(`drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/${revisionId}`, {
|
|
230
250
|
ManifestSignature: options.armoredManifestSignature,
|
|
231
251
|
SignatureAddress: options.signatureEmail,
|
|
232
|
-
XAttr: options.armoredExtendedAttributes
|
|
252
|
+
XAttr: options.armoredExtendedAttributes,
|
|
233
253
|
Photo: null, // Only used for photos in the Photo volume.
|
|
234
254
|
});
|
|
235
255
|
}
|
|
@@ -285,4 +305,149 @@ export class UploadAPIService {
|
|
|
285
305
|
link.File?.ActiveRevision?.RevisionID === revisionId
|
|
286
306
|
);
|
|
287
307
|
}
|
|
308
|
+
|
|
309
|
+
async uploadSmallFile(
|
|
310
|
+
parentFolderUid: string,
|
|
311
|
+
metadata: {
|
|
312
|
+
armoredEncryptedName: string;
|
|
313
|
+
hash: string;
|
|
314
|
+
mediaType: string;
|
|
315
|
+
armoredNodeKey: string;
|
|
316
|
+
armoredNodePassphrase: string;
|
|
317
|
+
armoredNodePassphraseSignature: string;
|
|
318
|
+
base64ContentKeyPacket: string;
|
|
319
|
+
armoredContentKeyPacketSignature: string;
|
|
320
|
+
armoredExtendedAttributes: string;
|
|
321
|
+
signatureEmail: string | AnonymousUser;
|
|
322
|
+
},
|
|
323
|
+
content: {
|
|
324
|
+
armoredManifestSignature: string;
|
|
325
|
+
block:
|
|
326
|
+
| {
|
|
327
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
328
|
+
armoredSignature: string;
|
|
329
|
+
verificationToken: Uint8Array<ArrayBuffer>;
|
|
330
|
+
}
|
|
331
|
+
| undefined;
|
|
332
|
+
thumbnails: {
|
|
333
|
+
type: ThumbnailType;
|
|
334
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
335
|
+
}[];
|
|
336
|
+
},
|
|
337
|
+
signal?: AbortSignal,
|
|
338
|
+
): Promise<{ nodeUid: string; nodeRevisionUid: string }> {
|
|
339
|
+
const { volumeId, nodeId: parentNodeId } = splitNodeUid(parentFolderUid);
|
|
340
|
+
|
|
341
|
+
const metadataPayload: PostSmallFileFormData['Metadata'] = {
|
|
342
|
+
ParentLinkID: parentNodeId,
|
|
343
|
+
Name: metadata.armoredEncryptedName,
|
|
344
|
+
NameHash: metadata.hash,
|
|
345
|
+
NodePassphrase: metadata.armoredNodePassphrase,
|
|
346
|
+
NodePassphraseSignature: metadata.armoredNodePassphraseSignature,
|
|
347
|
+
SignatureEmail: metadata.signatureEmail,
|
|
348
|
+
NodeKey: metadata.armoredNodeKey,
|
|
349
|
+
MIMEType: metadata.mediaType,
|
|
350
|
+
ContentKeyPacket: metadata.base64ContentKeyPacket,
|
|
351
|
+
ContentKeyPacketSignature: metadata.armoredContentKeyPacketSignature,
|
|
352
|
+
ManifestSignature: content.armoredManifestSignature,
|
|
353
|
+
ContentBlockEncSignature: content.block ? content.block.armoredSignature : null,
|
|
354
|
+
ContentBlockVerificationToken: content.block
|
|
355
|
+
? uint8ArrayToBase64String(content.block.verificationToken)
|
|
356
|
+
: null,
|
|
357
|
+
XAttr: metadata.armoredExtendedAttributes,
|
|
358
|
+
Photo: null, // TODO
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const formData = new FormData();
|
|
362
|
+
formData.set('Metadata', new Blob([JSON.stringify(metadataPayload)], { type: 'application/json' }), 'Metadata');
|
|
363
|
+
if (content.block) {
|
|
364
|
+
formData.set('ContentBlock', new Blob([content.block.encryptedData]), 'ContentBlock');
|
|
365
|
+
}
|
|
366
|
+
for (const thumb of content.thumbnails) {
|
|
367
|
+
if (formData.get(`ThumbnailBlockType_${thumb.type}`)) {
|
|
368
|
+
throw new Error('Duplicate thumbnail types');
|
|
369
|
+
}
|
|
370
|
+
formData.set(
|
|
371
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
372
|
+
new Blob([thumb.encryptedData]),
|
|
373
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result = await this.apiService.postFormData<PostSmallFileResponse>(
|
|
378
|
+
`drive/v2/volumes/${volumeId}/files/small`,
|
|
379
|
+
formData,
|
|
380
|
+
signal,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
nodeUid: makeNodeUid(volumeId, result.LinkID),
|
|
385
|
+
nodeRevisionUid: makeNodeRevisionUid(volumeId, result.LinkID, result.RevisionID),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async uploadSmallRevision(
|
|
390
|
+
nodeUid: string,
|
|
391
|
+
currentRevisionUid: string,
|
|
392
|
+
metadata: {
|
|
393
|
+
signatureEmail: string | AnonymousUser | null;
|
|
394
|
+
armoredExtendedAttributes: string;
|
|
395
|
+
},
|
|
396
|
+
content: {
|
|
397
|
+
armoredManifestSignature: string;
|
|
398
|
+
block:
|
|
399
|
+
| {
|
|
400
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
401
|
+
armoredSignature: string;
|
|
402
|
+
verificationToken: Uint8Array<ArrayBuffer>;
|
|
403
|
+
}
|
|
404
|
+
| undefined;
|
|
405
|
+
thumbnails: {
|
|
406
|
+
type: ThumbnailType;
|
|
407
|
+
encryptedData: Uint8Array<ArrayBuffer>;
|
|
408
|
+
}[];
|
|
409
|
+
},
|
|
410
|
+
signal?: AbortSignal,
|
|
411
|
+
): Promise<{ nodeUid: string; nodeRevisionUid: string }> {
|
|
412
|
+
const { volumeId, nodeId } = splitNodeUid(nodeUid);
|
|
413
|
+
const { revisionId: currentRevisionId } = splitNodeRevisionUid(currentRevisionUid);
|
|
414
|
+
|
|
415
|
+
const metadataPayload: PostSmallRevisionFormData['Metadata'] = {
|
|
416
|
+
CurrentRevisionID: currentRevisionId,
|
|
417
|
+
SignatureEmail: metadata.signatureEmail,
|
|
418
|
+
ManifestSignature: content.armoredManifestSignature,
|
|
419
|
+
ContentBlockEncSignature: content.block ? content.block.armoredSignature : null,
|
|
420
|
+
ContentBlockVerificationToken: content.block
|
|
421
|
+
? uint8ArrayToBase64String(content.block.verificationToken)
|
|
422
|
+
: null,
|
|
423
|
+
XAttr: metadata.armoredExtendedAttributes,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const formData = new FormData();
|
|
427
|
+
formData.set('Metadata', new Blob([JSON.stringify(metadataPayload)], { type: 'application/json' }), 'Metadata');
|
|
428
|
+
if (content.block) {
|
|
429
|
+
formData.set('ContentBlock', new Blob([content.block.encryptedData]), 'ContentBlock');
|
|
430
|
+
}
|
|
431
|
+
for (const thumb of content.thumbnails) {
|
|
432
|
+
if (formData.get(`ThumbnailBlockType_${thumb.type}`)) {
|
|
433
|
+
throw new Error('Duplicate thumbnail types');
|
|
434
|
+
}
|
|
435
|
+
formData.set(
|
|
436
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
437
|
+
new Blob([thumb.encryptedData]),
|
|
438
|
+
`ThumbnailBlockType_${thumb.type}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const result = await this.apiService.postFormData<PostSmallRevisionResponse>(
|
|
443
|
+
`drive/v2/volumes/${volumeId}/files/${nodeId}/revisions/small`,
|
|
444
|
+
formData,
|
|
445
|
+
signal,
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
nodeUid: makeNodeUid(volumeId, result.LinkID),
|
|
450
|
+
nodeRevisionUid: makeNodeRevisionUid(volumeId, result.LinkID, result.RevisionID),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
288
453
|
}
|
|
@@ -2,6 +2,18 @@ import { PrivateKey, SessionKey } from '../../crypto';
|
|
|
2
2
|
import { UploadAPIService } from './apiService';
|
|
3
3
|
import { UploadCryptoService } from './cryptoService';
|
|
4
4
|
|
|
5
|
+
export async function verifyBlockWithContentKey(
|
|
6
|
+
cryptoService: UploadCryptoService,
|
|
7
|
+
contentKeyPacket: Uint8Array<ArrayBuffer>,
|
|
8
|
+
contentKeyPacketSessionKey: SessionKey,
|
|
9
|
+
encryptedBlock: Uint8Array<ArrayBuffer>,
|
|
10
|
+
): Promise<{
|
|
11
|
+
verificationToken: Uint8Array<ArrayBuffer>;
|
|
12
|
+
}> {
|
|
13
|
+
const verificationCode = contentKeyPacket.subarray(-32);
|
|
14
|
+
return cryptoService.verifyBlock(contentKeyPacketSessionKey, verificationCode, encryptedBlock);
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
export class BlockVerifier {
|
|
6
18
|
private verificationCode?: Uint8Array<ArrayBuffer>;
|
|
7
19
|
private contentKeyPacketSessionKey?: SessionKey;
|
|
@@ -145,7 +145,9 @@ export class UploadCryptoService {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
async encryptBlock(
|
|
148
|
-
verifyBlock: (
|
|
148
|
+
verifyBlock: (
|
|
149
|
+
encryptedBlock: Uint8Array<ArrayBuffer>,
|
|
150
|
+
) => Promise<{ verificationToken: Uint8Array<ArrayBuffer> }>,
|
|
149
151
|
nodeRevisionDraftKeys: NodeRevisionDraftKeys,
|
|
150
152
|
block: Uint8Array<ArrayBuffer>,
|
|
151
153
|
index: number,
|
|
@@ -173,24 +175,22 @@ export class UploadCryptoService {
|
|
|
173
175
|
async commitFile(
|
|
174
176
|
nodeRevisionDraftKeys: NodeRevisionDraftKeys,
|
|
175
177
|
manifest: Uint8Array<ArrayBuffer>,
|
|
176
|
-
extendedAttributes
|
|
178
|
+
extendedAttributes: string,
|
|
177
179
|
): Promise<{
|
|
178
180
|
armoredManifestSignature: string;
|
|
179
181
|
signatureEmail: string | AnonymousUser;
|
|
180
|
-
armoredExtendedAttributes
|
|
182
|
+
armoredExtendedAttributes: string;
|
|
181
183
|
}> {
|
|
182
184
|
const { armoredManifestSignature } = await this.driveCrypto.signManifest(
|
|
183
185
|
manifest,
|
|
184
186
|
nodeRevisionDraftKeys.signingKeys.contentSigningKey,
|
|
185
187
|
);
|
|
186
188
|
|
|
187
|
-
const { armoredExtendedAttributes } =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
: { armoredExtendedAttributes: undefined };
|
|
189
|
+
const { armoredExtendedAttributes } = await this.driveCrypto.encryptExtendedAttributes(
|
|
190
|
+
extendedAttributes,
|
|
191
|
+
nodeRevisionDraftKeys.key,
|
|
192
|
+
nodeRevisionDraftKeys.signingKeys.contentSigningKey,
|
|
193
|
+
);
|
|
194
194
|
|
|
195
195
|
return {
|
|
196
196
|
armoredManifestSignature,
|
|
@@ -13,9 +13,9 @@ import { UploadTelemetry } from './telemetry';
|
|
|
13
13
|
* and initiate the upload process for a file object or a stream.
|
|
14
14
|
*
|
|
15
15
|
* This class is not meant to be used directly, but rather to be extended
|
|
16
|
-
* by `FileUploader`
|
|
16
|
+
* by `FileUploader`, `FileRevisionUploader`, or `SmallFileUploader`.
|
|
17
17
|
*/
|
|
18
|
-
abstract class Uploader {
|
|
18
|
+
export abstract class Uploader {
|
|
19
19
|
protected controller: UploadController;
|
|
20
20
|
protected abortController: AbortController;
|
|
21
21
|
|
|
@@ -51,9 +51,9 @@ abstract class Uploader {
|
|
|
51
51
|
thumbnails: Thumbnail[],
|
|
52
52
|
onProgress?: (uploadedBytes: number) => void,
|
|
53
53
|
): Promise<UploadController> {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
this.assertNotStartedYet();
|
|
55
|
+
this.assertUniqueThumbnailTypes(thumbnails);
|
|
56
|
+
|
|
57
57
|
if (!this.metadata.mediaType) {
|
|
58
58
|
this.metadata.mediaType = fileObject.type;
|
|
59
59
|
}
|
|
@@ -72,11 +72,24 @@ abstract class Uploader {
|
|
|
72
72
|
thumbnails: Thumbnail[],
|
|
73
73
|
onProgress?: (uploadedBytes: number) => void,
|
|
74
74
|
): Promise<UploadController> {
|
|
75
|
+
this.assertNotStartedYet();
|
|
76
|
+
this.assertUniqueThumbnailTypes(thumbnails);
|
|
77
|
+
|
|
78
|
+
this.controller.promise = this.startUpload(stream, thumbnails, onProgress);
|
|
79
|
+
return this.controller;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private assertNotStartedYet(): void {
|
|
75
83
|
if (this.controller.promise) {
|
|
76
84
|
throw new Error(`Upload already started`);
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private assertUniqueThumbnailTypes(thumbnails: Thumbnail[]): void {
|
|
89
|
+
const uniqueThumbnailTypes = new Set(thumbnails.map(({ type }) => type));
|
|
90
|
+
if (uniqueThumbnailTypes.size !== thumbnails.length) {
|
|
91
|
+
throw new Error('Duplicate thumbnail types');
|
|
92
|
+
}
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
protected async startUpload(
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { FeatureFlagProvider, FeatureFlags, UploadMetadata } from '../../interface';
|
|
2
|
+
import { getMockTelemetry } from '../../tests/telemetry';
|
|
3
|
+
import { FileRevisionUploader, FileUploader } from './fileUploader';
|
|
4
|
+
import { initUploadModule } from './index';
|
|
5
|
+
import { SmallFileRevisionUploader, SmallFileUploader } from './smallFileUploader';
|
|
6
|
+
|
|
7
|
+
const SMALL_FILE_SIZE_LIMIT = 128 * 1024; // 128 KiB, must match index.ts
|
|
8
|
+
|
|
9
|
+
describe('initUploadModule - uploader selection', () => {
|
|
10
|
+
const parentFolderUid = 'parent-folder-uid';
|
|
11
|
+
const name = 'test-file.txt';
|
|
12
|
+
const nodeUid = 'node-uid';
|
|
13
|
+
|
|
14
|
+
let featureFlagProvider: jest.Mocked<FeatureFlagProvider>;
|
|
15
|
+
let uploadModule: ReturnType<typeof initUploadModule>;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
const apiService = {};
|
|
19
|
+
const driveCrypto = {};
|
|
20
|
+
const sharesService = {};
|
|
21
|
+
const nodesService = {};
|
|
22
|
+
featureFlagProvider = {
|
|
23
|
+
isEnabled: jest.fn().mockResolvedValue(true),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
uploadModule = initUploadModule(
|
|
27
|
+
getMockTelemetry(),
|
|
28
|
+
apiService as any,
|
|
29
|
+
driveCrypto as any,
|
|
30
|
+
sharesService as any,
|
|
31
|
+
nodesService as any,
|
|
32
|
+
featureFlagProvider as any,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('getFileUploader', () => {
|
|
37
|
+
it('returns SmallFileUploader when feature flag is enabled and file size is below limit', async () => {
|
|
38
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
39
|
+
|
|
40
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
41
|
+
const uploader = await uploadModule.getFileUploader(parentFolderUid, name, metadata);
|
|
42
|
+
|
|
43
|
+
expect(uploader).toBeInstanceOf(SmallFileUploader);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns FileUploader when feature flag is enabled but file size exceeds limit', async () => {
|
|
47
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
48
|
+
|
|
49
|
+
const metadata: UploadMetadata = {
|
|
50
|
+
expectedSize: SMALL_FILE_SIZE_LIMIT,
|
|
51
|
+
mediaType: 'text/plain',
|
|
52
|
+
};
|
|
53
|
+
const uploader = await uploadModule.getFileUploader(parentFolderUid, name, metadata);
|
|
54
|
+
|
|
55
|
+
expect(uploader).toBeInstanceOf(FileUploader);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns FileUploader when feature flag is disabled even for small file', async () => {
|
|
59
|
+
featureFlagProvider.isEnabled.mockResolvedValue(false);
|
|
60
|
+
|
|
61
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
62
|
+
const uploader = await uploadModule.getFileUploader(parentFolderUid, name, metadata);
|
|
63
|
+
|
|
64
|
+
expect(uploader).toBeInstanceOf(FileUploader);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('getFileRevisionUploader', () => {
|
|
69
|
+
it('returns SmallFileRevisionUploader when feature flag is enabled and file size is below limit', async () => {
|
|
70
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
71
|
+
|
|
72
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
73
|
+
const uploader = await uploadModule.getFileRevisionUploader(nodeUid, metadata);
|
|
74
|
+
|
|
75
|
+
expect(uploader).toBeInstanceOf(SmallFileRevisionUploader);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns FileRevisionUploader when feature flag is enabled but file size exceeds limit', async () => {
|
|
79
|
+
featureFlagProvider.isEnabled.mockResolvedValue(true);
|
|
80
|
+
|
|
81
|
+
const metadata: UploadMetadata = {
|
|
82
|
+
expectedSize: SMALL_FILE_SIZE_LIMIT + 1,
|
|
83
|
+
mediaType: 'text/plain',
|
|
84
|
+
};
|
|
85
|
+
const uploader = await uploadModule.getFileRevisionUploader(nodeUid, metadata);
|
|
86
|
+
|
|
87
|
+
expect(uploader).toBeInstanceOf(FileRevisionUploader);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns FileRevisionUploader when feature flag is disabled even for small file', async () => {
|
|
91
|
+
featureFlagProvider.isEnabled.mockResolvedValue(false);
|
|
92
|
+
|
|
93
|
+
const metadata: UploadMetadata = { expectedSize: 1, mediaType: 'text/plain' };
|
|
94
|
+
const uploader = await uploadModule.getFileRevisionUploader(nodeUid, metadata);
|
|
95
|
+
|
|
96
|
+
expect(uploader).toBeInstanceOf(FileRevisionUploader);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|