@protontech/drive-sdk 0.12.1 → 0.13.0

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.
Files changed (124) hide show
  1. package/dist/crypto/driveCrypto.d.ts +1 -0
  2. package/dist/crypto/driveCrypto.js +1 -0
  3. package/dist/crypto/driveCrypto.js.map +1 -1
  4. package/dist/interface/featureFlags.d.ts +2 -1
  5. package/dist/interface/featureFlags.js +1 -0
  6. package/dist/interface/featureFlags.js.map +1 -1
  7. package/dist/interface/httpClient.d.ts +1 -0
  8. package/dist/interface/nodes.d.ts +7 -0
  9. package/dist/interface/nodes.js.map +1 -1
  10. package/dist/internal/apiService/apiService.d.ts +1 -0
  11. package/dist/internal/apiService/apiService.js +16 -7
  12. package/dist/internal/apiService/apiService.js.map +1 -1
  13. package/dist/internal/apiService/apiService.test.js +24 -0
  14. package/dist/internal/apiService/apiService.test.js.map +1 -1
  15. package/dist/internal/apiService/driveTypes.d.ts +110 -101
  16. package/dist/internal/nodes/apiService.d.ts +4 -0
  17. package/dist/internal/nodes/apiService.js +4 -0
  18. package/dist/internal/nodes/apiService.js.map +1 -1
  19. package/dist/internal/nodes/apiService.test.js +8 -0
  20. package/dist/internal/nodes/apiService.test.js.map +1 -1
  21. package/dist/internal/nodes/cryptoService.js +3 -0
  22. package/dist/internal/nodes/cryptoService.js.map +1 -1
  23. package/dist/internal/nodes/extendedAttributes.d.ts +5 -5
  24. package/dist/internal/nodes/extendedAttributes.js +5 -14
  25. package/dist/internal/nodes/extendedAttributes.js.map +1 -1
  26. package/dist/internal/nodes/extendedAttributes.test.js +16 -22
  27. package/dist/internal/nodes/extendedAttributes.test.js.map +1 -1
  28. package/dist/internal/nodes/interface.d.ts +5 -0
  29. package/dist/internal/nodes/nodesManagement.d.ts +3 -3
  30. package/dist/internal/nodes/nodesManagement.js +7 -5
  31. package/dist/internal/nodes/nodesManagement.js.map +1 -1
  32. package/dist/internal/photos/albumsManager.js +1 -0
  33. package/dist/internal/photos/albumsManager.js.map +1 -1
  34. package/dist/internal/photos/nodes.d.ts +1 -1
  35. package/dist/internal/photos/nodes.js +2 -2
  36. package/dist/internal/photos/nodes.js.map +1 -1
  37. package/dist/internal/photos/upload.d.ts +5 -5
  38. package/dist/internal/photos/upload.js +8 -2
  39. package/dist/internal/photos/upload.js.map +1 -1
  40. package/dist/internal/sharingPublic/nodes.d.ts +1 -0
  41. package/dist/internal/upload/apiService.d.ts +45 -1
  42. package/dist/internal/upload/apiService.js +69 -1
  43. package/dist/internal/upload/apiService.js.map +1 -1
  44. package/dist/internal/upload/blockVerifier.d.ts +4 -1
  45. package/dist/internal/upload/blockVerifier.js +5 -0
  46. package/dist/internal/upload/blockVerifier.js.map +1 -1
  47. package/dist/internal/upload/cryptoService.d.ts +2 -2
  48. package/dist/internal/upload/cryptoService.js +1 -3
  49. package/dist/internal/upload/cryptoService.js.map +1 -1
  50. package/dist/internal/upload/fileUploader.d.ts +4 -3
  51. package/dist/internal/upload/fileUploader.js +17 -7
  52. package/dist/internal/upload/fileUploader.js.map +1 -1
  53. package/dist/internal/upload/index.d.ts +3 -3
  54. package/dist/internal/upload/index.js +17 -1
  55. package/dist/internal/upload/index.js.map +1 -1
  56. package/dist/internal/upload/index.test.d.ts +1 -0
  57. package/dist/internal/upload/index.test.js +71 -0
  58. package/dist/internal/upload/index.test.js.map +1 -0
  59. package/dist/internal/upload/interface.d.ts +2 -0
  60. package/dist/internal/upload/manager.d.ts +41 -2
  61. package/dist/internal/upload/manager.js +123 -42
  62. package/dist/internal/upload/manager.js.map +1 -1
  63. package/dist/internal/upload/manager.test.js +267 -0
  64. package/dist/internal/upload/manager.test.js.map +1 -1
  65. package/dist/internal/upload/smallFileUploader.d.ts +83 -0
  66. package/dist/internal/upload/smallFileUploader.js +197 -0
  67. package/dist/internal/upload/smallFileUploader.js.map +1 -0
  68. package/dist/internal/upload/smallFileUploader.test.d.ts +1 -0
  69. package/dist/internal/upload/smallFileUploader.test.js +358 -0
  70. package/dist/internal/upload/smallFileUploader.test.js.map +1 -0
  71. package/dist/internal/upload/streamReader.d.ts +4 -0
  72. package/dist/internal/upload/streamReader.js +37 -0
  73. package/dist/internal/upload/streamReader.js.map +1 -0
  74. package/dist/internal/upload/streamReader.test.d.ts +1 -0
  75. package/dist/internal/upload/streamReader.test.js +90 -0
  76. package/dist/internal/upload/streamReader.test.js.map +1 -0
  77. package/dist/internal/upload/streamUploader.d.ts +6 -0
  78. package/dist/internal/upload/streamUploader.js +3 -3
  79. package/dist/internal/upload/streamUploader.js.map +1 -1
  80. package/dist/internal/upload/telemetry.d.ts +3 -2
  81. package/dist/internal/upload/telemetry.js +3 -0
  82. package/dist/internal/upload/telemetry.js.map +1 -1
  83. package/dist/protonDrivePhotosClient.d.ts +1 -1
  84. package/dist/protonDrivePublicLinkClient.js +3 -1
  85. package/dist/protonDrivePublicLinkClient.js.map +1 -1
  86. package/dist/transformers.d.ts +1 -1
  87. package/dist/transformers.js +1 -0
  88. package/dist/transformers.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/crypto/driveCrypto.ts +2 -0
  91. package/src/interface/featureFlags.ts +1 -0
  92. package/src/interface/httpClient.ts +1 -0
  93. package/src/interface/nodes.ts +7 -0
  94. package/src/internal/apiService/apiService.test.ts +30 -0
  95. package/src/internal/apiService/apiService.ts +23 -7
  96. package/src/internal/apiService/driveTypes.ts +110 -101
  97. package/src/internal/nodes/apiService.test.ts +9 -0
  98. package/src/internal/nodes/apiService.ts +4 -0
  99. package/src/internal/nodes/cryptoService.ts +11 -1
  100. package/src/internal/nodes/extendedAttributes.test.ts +25 -25
  101. package/src/internal/nodes/extendedAttributes.ts +10 -19
  102. package/src/internal/nodes/interface.ts +5 -0
  103. package/src/internal/nodes/nodesManagement.ts +8 -6
  104. package/src/internal/photos/albumsManager.ts +1 -0
  105. package/src/internal/photos/nodes.ts +2 -2
  106. package/src/internal/photos/upload.ts +23 -10
  107. package/src/internal/upload/apiService.ts +167 -2
  108. package/src/internal/upload/blockVerifier.ts +12 -0
  109. package/src/internal/upload/cryptoService.ts +10 -10
  110. package/src/internal/upload/fileUploader.ts +20 -7
  111. package/src/internal/upload/index.test.ts +99 -0
  112. package/src/internal/upload/index.ts +45 -4
  113. package/src/internal/upload/interface.ts +2 -0
  114. package/src/internal/upload/manager.test.ts +367 -1
  115. package/src/internal/upload/manager.ts +226 -76
  116. package/src/internal/upload/smallFileUploader.test.ts +491 -0
  117. package/src/internal/upload/smallFileUploader.ts +353 -0
  118. package/src/internal/upload/streamReader.test.ts +109 -0
  119. package/src/internal/upload/streamReader.ts +38 -0
  120. package/src/internal/upload/streamUploader.ts +1 -1
  121. package/src/internal/upload/telemetry.ts +5 -1
  122. package/src/protonDrivePhotosClient.ts +1 -1
  123. package/src/protonDrivePublicLinkClient.ts +2 -0
  124. package/src/transformers.ts +2 -0
@@ -1,6 +1,7 @@
1
1
  import { c } from 'ttag';
2
2
 
3
- import { Logger, ProtonDriveTelemetry, UploadMetadata } from '../../interface';
3
+ import { PrivateKey, SessionKey } from '../../crypto';
4
+ import { Logger, ProtonDriveTelemetry, ThumbnailType, UploadMetadata } from '../../interface';
4
5
  import { ValidationError, NodeWithSameNameExistsValidationError } from '../../errors';
5
6
  import { ErrorCode } from '../apiService';
6
7
  import { generateFileExtendedAttributes } from '../nodes';
@@ -32,20 +33,11 @@ export class UploadManager {
32
33
  }
33
34
 
34
35
  async createDraftNode(parentFolderUid: string, name: string, metadata: UploadMetadata): Promise<NodeRevisionDraft> {
35
- const parentKeys = await this.nodesService.getNodeKeys(parentFolderUid);
36
- if (!parentKeys.hashKey) {
37
- throw new ValidationError(c('Error').t`Creating files in non-folders is not allowed`);
38
- }
39
-
40
- const generatedNodeCrypto = await this.cryptoService.generateFileCrypto(
41
- parentFolderUid,
42
- { key: parentKeys.key, hashKey: parentKeys.hashKey },
43
- name,
44
- );
36
+ const { parentHashKey, ...generatedNodeCrypto } = await this.generateNewFileCrypto(parentFolderUid, name);
45
37
 
46
38
  const { nodeUid, nodeRevisionUid } = await this.createDraftOnAPI(
47
39
  parentFolderUid,
48
- parentKeys.hashKey,
40
+ parentHashKey,
49
41
  name,
50
42
  metadata,
51
43
  generatedNodeCrypto,
@@ -60,7 +52,7 @@ export class UploadManager {
60
52
  signingKeys: generatedNodeCrypto.signingKeys,
61
53
  },
62
54
  parentNodeKeys: {
63
- hashKey: parentKeys.hashKey,
55
+ hashKey: parentHashKey,
64
56
  },
65
57
  newNodeInfo: {
66
58
  parentUid: parentFolderUid,
@@ -71,6 +63,57 @@ export class UploadManager {
71
63
  };
72
64
  }
73
65
 
66
+ async generateNewFileCrypto(
67
+ parentFolderUid: string,
68
+ name: string,
69
+ ): Promise<NodeCrypto & { parentHashKey: Uint8Array<ArrayBuffer> }> {
70
+ const parentKeys = await this.nodesService.getNodeKeys(parentFolderUid);
71
+ if (!parentKeys.hashKey) {
72
+ throw new ValidationError(c('Error').t`Creating files in non-folders is not allowed`);
73
+ }
74
+
75
+ const generatedNodeCrypto = await this.cryptoService.generateFileCrypto(
76
+ parentFolderUid,
77
+ { key: parentKeys.key, hashKey: parentKeys.hashKey },
78
+ name,
79
+ );
80
+
81
+ return {
82
+ ...generatedNodeCrypto,
83
+ parentHashKey: parentKeys.hashKey,
84
+ };
85
+ }
86
+
87
+ async getExistingFileNodeCrypto(nodeUid: string): Promise<{
88
+ key: PrivateKey;
89
+ contentKeyPacket: Uint8Array<ArrayBuffer>;
90
+ contentKeyPacketSessionKey: SessionKey;
91
+ signingKeys: NodeCrypto['signingKeys'];
92
+ }> {
93
+ const node = await this.nodesService.getNode(nodeUid);
94
+ const nodeKeys = await this.nodesService.getNodeKeys(nodeUid);
95
+
96
+ if (!node.activeRevision?.ok || !nodeKeys.contentKeyPacketSessionKey) {
97
+ throw new ValidationError(c('Error').t`Creating revisions in non-files is not allowed`);
98
+ }
99
+
100
+ if (!nodeKeys.contentKeyPacket) {
101
+ throw new ValidationError(c('Error').t`Content key packet is required for small revision upload`);
102
+ }
103
+
104
+ const signingKeys = await this.cryptoService.getSigningKeysForExistingNode({
105
+ nodeUid,
106
+ parentNodeUid: node.parentUid,
107
+ });
108
+
109
+ return {
110
+ key: nodeKeys.key,
111
+ contentKeyPacket: nodeKeys.contentKeyPacket,
112
+ contentKeyPacketSessionKey: nodeKeys.contentKeyPacketSessionKey,
113
+ signingKeys,
114
+ };
115
+ }
116
+
74
117
  private async createDraftOnAPI(
75
118
  parentFolderUid: string,
76
119
  parentHashKey: Uint8Array<ArrayBuffer>,
@@ -97,80 +140,187 @@ export class UploadManager {
97
140
  });
98
141
  return result;
99
142
  } catch (error: unknown) {
100
- if (error instanceof ValidationError) {
101
- if (error.code === ErrorCode.ALREADY_EXISTS) {
102
- this.logger.info(`Node with given name already exists`);
103
-
104
- const typedDetails = error.details as
105
- | {
106
- ConflictLinkID: string;
107
- ConflictRevisionID?: string;
108
- ConflictDraftRevisionID?: string;
109
- ConflictDraftClientUID?: string;
143
+ return this.handleConflictError(parentFolderUid, metadata, error, async () => {
144
+ return this.createDraftOnAPI(parentFolderUid, parentHashKey, name, metadata, generatedNodeCrypto);
145
+ });
146
+ }
147
+ }
148
+
149
+ async uploadFile(
150
+ parentFolderUid: string,
151
+ nodeCrypto: NodeCrypto,
152
+ metadata: UploadMetadata,
153
+ commitPayload: {
154
+ armoredManifestSignature: string;
155
+ armoredExtendedAttributes: string;
156
+ },
157
+ encryptedBlock:
158
+ | {
159
+ encryptedData: Uint8Array<ArrayBuffer>;
160
+ armoredSignature: string;
161
+ verificationToken: Uint8Array<ArrayBuffer>;
162
+ }
163
+ | undefined,
164
+ encryptedThumbnails: { type: ThumbnailType; encryptedData: Uint8Array<ArrayBuffer> }[],
165
+ signal?: AbortSignal,
166
+ ): Promise<{ nodeUid: string; nodeRevisionUid: string }> {
167
+ try {
168
+ const result = await this.apiService.uploadSmallFile(
169
+ parentFolderUid,
170
+ {
171
+ armoredEncryptedName: nodeCrypto.encryptedNode.encryptedName,
172
+ hash: nodeCrypto.encryptedNode.hash,
173
+ mediaType: metadata.mediaType ?? 'application/octet-stream',
174
+ armoredNodeKey: nodeCrypto.nodeKeys.encrypted.armoredKey,
175
+ armoredNodePassphrase: nodeCrypto.nodeKeys.encrypted.armoredPassphrase,
176
+ armoredNodePassphraseSignature: nodeCrypto.nodeKeys.encrypted.armoredPassphraseSignature,
177
+ base64ContentKeyPacket: nodeCrypto.contentKey.encrypted.base64ContentKeyPacket,
178
+ armoredContentKeyPacketSignature: nodeCrypto.contentKey.encrypted.armoredContentKeyPacketSignature,
179
+ armoredExtendedAttributes: commitPayload.armoredExtendedAttributes,
180
+ signatureEmail: nodeCrypto.signingKeys.email ?? null,
181
+ },
182
+ {
183
+ armoredManifestSignature: commitPayload.armoredManifestSignature,
184
+ block: encryptedBlock
185
+ ? {
186
+ encryptedData: encryptedBlock.encryptedData,
187
+ armoredSignature: encryptedBlock.armoredSignature,
188
+ verificationToken: encryptedBlock.verificationToken,
110
189
  }
111
- | undefined;
112
-
113
- // If the client doesn't specify the client UID, it should
114
- // never be considered own draft.
115
- const isOwnDraftConflict =
116
- typedDetails?.ConflictDraftRevisionID &&
117
- this.clientUid &&
118
- typedDetails?.ConflictDraftClientUID === this.clientUid;
119
-
120
- // If there is existing draft created by this client,
121
- // automatically delete it and try to create a new one
122
- // with the same name again.
123
- if (
124
- typedDetails?.ConflictDraftRevisionID &&
125
- (isOwnDraftConflict || metadata.overrideExistingDraftByOtherClient)
126
- ) {
127
- const existingDraftNodeUid = makeNodeUid(
128
- splitNodeUid(parentFolderUid).volumeId,
129
- typedDetails.ConflictLinkID,
130
- );
190
+ : undefined,
191
+ thumbnails: encryptedThumbnails,
192
+ },
193
+ signal,
194
+ );
195
+ await this.nodesService.notifyChildCreated(parentFolderUid);
196
+ return result;
197
+ } catch (error: unknown) {
198
+ return this.handleConflictError(parentFolderUid, metadata, error, async () => {
199
+ return this.uploadFile(
200
+ parentFolderUid,
201
+ nodeCrypto,
202
+ metadata,
203
+ commitPayload,
204
+ encryptedBlock,
205
+ encryptedThumbnails,
206
+ signal,
207
+ );
208
+ });
209
+ }
210
+ }
131
211
 
132
- let deleteFailed = false;
133
- try {
134
- this.logger.warn(
135
- `Deleting existing draft node ${existingDraftNodeUid} by ${typedDetails.ConflictDraftClientUID}`,
136
- );
137
- await this.apiService.deleteDraft(existingDraftNodeUid);
138
- } catch (deleteDraftError: unknown) {
139
- // Do not throw, let throw the conflict error.
140
- deleteFailed = true;
141
- this.logger.error('Failed to delete existing draft node', deleteDraftError);
142
- }
143
- if (!deleteFailed) {
144
- return this.createDraftOnAPI(
145
- parentFolderUid,
146
- parentHashKey,
147
- name,
148
- metadata,
149
- generatedNodeCrypto,
150
- );
151
- }
152
- }
212
+ async uploadSmallRevision(
213
+ nodeUid: string,
214
+ nodeCrypto: Pick<NodeCrypto, 'signingKeys'>,
215
+ commitPayload: {
216
+ armoredManifestSignature: string;
217
+ armoredExtendedAttributes: string;
218
+ },
219
+ encryptedBlock:
220
+ | {
221
+ encryptedData: Uint8Array<ArrayBuffer>;
222
+ armoredSignature: string;
223
+ verificationToken: Uint8Array<ArrayBuffer>;
224
+ }
225
+ | undefined,
226
+ encryptedThumbnails: { type: ThumbnailType; encryptedData: Uint8Array<ArrayBuffer> }[],
227
+ signal?: AbortSignal,
228
+ ): Promise<{ nodeUid: string; nodeRevisionUid: string }> {
229
+ const node = await this.nodesService.getNode(nodeUid);
230
+ if (!node.activeRevision?.ok) {
231
+ throw new ValidationError(c('Error').t`File has no revision`);
232
+ }
233
+ const result = await this.apiService.uploadSmallRevision(
234
+ nodeUid,
235
+ node.activeRevision.value.uid,
236
+ {
237
+ signatureEmail: nodeCrypto.signingKeys.email ?? null,
238
+ armoredExtendedAttributes: commitPayload.armoredExtendedAttributes,
239
+ },
240
+ {
241
+ armoredManifestSignature: commitPayload.armoredManifestSignature,
242
+ block: encryptedBlock,
243
+ thumbnails: encryptedThumbnails,
244
+ },
245
+ signal,
246
+ );
247
+ await this.nodesService.notifyNodeChanged(nodeUid);
248
+ return result;
249
+ }
250
+
251
+ private async handleConflictError(
252
+ parentFolderUid: string,
253
+ metadata: UploadMetadata,
254
+ error: unknown,
255
+ onRetryAfterDraftDeleted: () => Promise<{ nodeUid: string; nodeRevisionUid: string }>,
256
+ ): Promise<{ nodeUid: string; nodeRevisionUid: string }> {
257
+ if (error instanceof ValidationError) {
258
+ if (error.code === ErrorCode.ALREADY_EXISTS) {
259
+ this.logger.info(`Node with given name already exists`);
260
+
261
+ const typedDetails = error.details as
262
+ | {
263
+ ConflictLinkID: string;
264
+ ConflictRevisionID?: string;
265
+ ConflictDraftRevisionID?: string;
266
+ ConflictDraftClientUID?: string;
267
+ }
268
+ | undefined;
269
+
270
+ // If the client doesn't specify the client UID, it should
271
+ // never be considered own draft.
272
+ const isOwnDraftConflict =
273
+ typedDetails?.ConflictDraftRevisionID &&
274
+ this.clientUid &&
275
+ typedDetails?.ConflictDraftClientUID === this.clientUid;
276
+
277
+ // If there is existing draft created by this client,
278
+ // automatically delete it and try to create a new one
279
+ // with the same name again.
280
+ if (
281
+ typedDetails?.ConflictDraftRevisionID &&
282
+ (isOwnDraftConflict || metadata.overrideExistingDraftByOtherClient)
283
+ ) {
284
+ const existingDraftNodeUid = makeNodeUid(
285
+ splitNodeUid(parentFolderUid).volumeId,
286
+ typedDetails.ConflictLinkID,
287
+ );
153
288
 
154
- if (isOwnDraftConflict) {
289
+ let deleteFailed = false;
290
+ try {
155
291
  this.logger.warn(
156
- `Existing draft conflict by another client ${typedDetails.ConflictDraftClientUID}`,
292
+ `Deleting existing draft node ${existingDraftNodeUid} by ${typedDetails.ConflictDraftClientUID}`,
157
293
  );
294
+ await this.apiService.deleteDraft(existingDraftNodeUid);
295
+ } catch (deleteDraftError: unknown) {
296
+ // Do not throw, let throw the conflict error.
297
+ deleteFailed = true;
298
+ this.logger.error('Failed to delete existing draft node', deleteDraftError);
158
299
  }
300
+ if (!deleteFailed) {
301
+ return onRetryAfterDraftDeleted();
302
+ }
303
+ }
159
304
 
160
- const existingNodeUid = typedDetails
161
- ? makeNodeUid(splitNodeUid(parentFolderUid).volumeId, typedDetails.ConflictLinkID)
162
- : undefined;
163
-
164
- throw new NodeWithSameNameExistsValidationError(
165
- error.message,
166
- error.code,
167
- existingNodeUid,
168
- !!typedDetails?.ConflictDraftRevisionID,
305
+ if (isOwnDraftConflict) {
306
+ this.logger.warn(
307
+ `Existing draft conflict by another client ${typedDetails.ConflictDraftClientUID}`,
169
308
  );
170
309
  }
310
+
311
+ const existingNodeUid = typedDetails
312
+ ? makeNodeUid(splitNodeUid(parentFolderUid).volumeId, typedDetails.ConflictLinkID)
313
+ : undefined;
314
+
315
+ throw new NodeWithSameNameExistsValidationError(
316
+ error.message,
317
+ error.code,
318
+ existingNodeUid,
319
+ !!typedDetails?.ConflictDraftRevisionID,
320
+ );
171
321
  }
172
- throw error;
173
322
  }
323
+ throw error;
174
324
  }
175
325
 
176
326
  async deleteDraftNode(nodeUid: string): Promise<void> {