@protontech/drive-sdk 0.0.11 → 0.0.13

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 (184) hide show
  1. package/dist/cache/index.d.ts +2 -0
  2. package/dist/cache/index.js +6 -0
  3. package/dist/cache/index.js.map +1 -0
  4. package/dist/cache/interface.d.ts +105 -0
  5. package/dist/cache/interface.js +3 -0
  6. package/dist/cache/interface.js.map +1 -0
  7. package/dist/cache/memoryCache.d.ts +18 -0
  8. package/dist/cache/memoryCache.js +78 -0
  9. package/dist/cache/memoryCache.js.map +1 -0
  10. package/dist/cache/memoryCache.test.d.ts +1 -0
  11. package/dist/cache/memoryCache.test.js +121 -0
  12. package/dist/cache/memoryCache.test.js.map +1 -0
  13. package/dist/crypto/hmac.d.ts +22 -0
  14. package/dist/crypto/hmac.js +44 -0
  15. package/dist/crypto/hmac.js.map +1 -0
  16. package/dist/crypto/utils.d.ts +2 -0
  17. package/dist/crypto/utils.js +35 -0
  18. package/dist/crypto/utils.js.map +1 -0
  19. package/dist/errors.d.ts +142 -0
  20. package/dist/errors.js +168 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/interface/account.js +3 -0
  23. package/dist/interface/account.js.map +1 -0
  24. package/dist/interface/author.d.ts +26 -0
  25. package/dist/interface/author.js +3 -0
  26. package/dist/interface/author.js.map +1 -0
  27. package/dist/interface/download.d.ts +29 -0
  28. package/dist/interface/download.js +3 -0
  29. package/dist/interface/download.js.map +1 -0
  30. package/dist/interface/httpClient.d.ts +38 -0
  31. package/dist/interface/httpClient.js +3 -0
  32. package/dist/interface/httpClient.js.map +1 -0
  33. package/dist/interface/index.d.ts +1 -1
  34. package/dist/interface/nodes.d.ts +12 -1
  35. package/dist/interface/nodes.js +11 -0
  36. package/dist/interface/nodes.js.map +1 -1
  37. package/dist/interface/result.d.ts +9 -0
  38. package/dist/interface/result.js +11 -0
  39. package/dist/interface/result.js.map +1 -0
  40. package/dist/interface/thumbnail.d.ts +17 -0
  41. package/dist/interface/thumbnail.js +9 -0
  42. package/dist/interface/thumbnail.js.map +1 -0
  43. package/dist/interface/upload.d.ts +64 -0
  44. package/dist/interface/upload.js +3 -0
  45. package/dist/interface/upload.js.map +1 -0
  46. package/dist/internal/apiService/driveTypes.d.ts +1341 -465
  47. package/dist/internal/apiService/errorCodes.d.ts +30 -0
  48. package/dist/internal/apiService/errorCodes.js +11 -0
  49. package/dist/internal/apiService/errorCodes.js.map +1 -0
  50. package/dist/internal/apiService/errors.js +2 -2
  51. package/dist/internal/apiService/errors.js.map +1 -1
  52. package/dist/internal/apiService/observerStream.d.ts +3 -0
  53. package/dist/internal/apiService/observerStream.js +15 -0
  54. package/dist/internal/apiService/observerStream.js.map +1 -0
  55. package/dist/internal/apiService/transformers.js +2 -0
  56. package/dist/internal/apiService/transformers.js.map +1 -1
  57. package/dist/internal/asyncIteratorMap.d.ts +15 -0
  58. package/dist/internal/asyncIteratorMap.js +59 -0
  59. package/dist/internal/asyncIteratorMap.js.map +1 -0
  60. package/dist/internal/asyncIteratorMap.test.d.ts +1 -0
  61. package/dist/internal/asyncIteratorMap.test.js +120 -0
  62. package/dist/internal/asyncIteratorMap.test.js.map +1 -0
  63. package/dist/internal/batchLoading.d.ts +34 -0
  64. package/dist/internal/batchLoading.js +68 -0
  65. package/dist/internal/batchLoading.js.map +1 -0
  66. package/dist/internal/batchLoading.test.d.ts +1 -0
  67. package/dist/internal/batchLoading.test.js +50 -0
  68. package/dist/internal/batchLoading.test.js.map +1 -0
  69. package/dist/internal/download/controller.d.ts +8 -0
  70. package/dist/internal/download/controller.js +22 -0
  71. package/dist/internal/download/controller.js.map +1 -0
  72. package/dist/internal/download/queue.d.ts +5 -0
  73. package/dist/internal/download/queue.js +31 -0
  74. package/dist/internal/download/queue.js.map +1 -0
  75. package/dist/internal/errors.js +28 -0
  76. package/dist/internal/errors.js.map +1 -0
  77. package/dist/internal/errors.test.js +22 -0
  78. package/dist/internal/errors.test.js.map +1 -0
  79. package/dist/internal/events/interface.d.ts +47 -0
  80. package/dist/internal/events/interface.js +12 -0
  81. package/dist/internal/events/interface.js.map +1 -0
  82. package/dist/internal/nodes/apiService.d.ts +2 -2
  83. package/dist/internal/nodes/apiService.js +16 -6
  84. package/dist/internal/nodes/apiService.js.map +1 -1
  85. package/dist/internal/nodes/apiService.test.js +30 -8
  86. package/dist/internal/nodes/apiService.test.js.map +1 -1
  87. package/dist/internal/nodes/cache.js +1 -0
  88. package/dist/internal/nodes/cache.js.map +1 -1
  89. package/dist/internal/nodes/cache.test.js +1 -0
  90. package/dist/internal/nodes/cache.test.js.map +1 -1
  91. package/dist/internal/nodes/cryptoService.test.js +34 -0
  92. package/dist/internal/nodes/cryptoService.test.js.map +1 -1
  93. package/dist/internal/nodes/index.test.js +3 -1
  94. package/dist/internal/nodes/index.test.js.map +1 -1
  95. package/dist/internal/nodes/interface.d.ts +3 -1
  96. package/dist/internal/nodes/mediaTypes.d.ts +2 -0
  97. package/dist/internal/nodes/mediaTypes.js +13 -0
  98. package/dist/internal/nodes/mediaTypes.js.map +1 -0
  99. package/dist/internal/nodes/nodesAccess.js +28 -7
  100. package/dist/internal/nodes/nodesAccess.js.map +1 -1
  101. package/dist/internal/nodes/nodesAccess.test.js +7 -6
  102. package/dist/internal/nodes/nodesAccess.test.js.map +1 -1
  103. package/dist/internal/nodes/validations.d.ts +4 -0
  104. package/dist/internal/nodes/validations.js +21 -0
  105. package/dist/internal/nodes/validations.js.map +1 -0
  106. package/dist/internal/sharing/apiService.js +19 -2
  107. package/dist/internal/sharing/apiService.js.map +1 -1
  108. package/dist/internal/uids.d.ts +38 -0
  109. package/dist/internal/uids.js +85 -0
  110. package/dist/internal/uids.js.map +1 -0
  111. package/dist/internal/upload/chunkStreamReader.d.ts +13 -0
  112. package/dist/internal/upload/chunkStreamReader.js +46 -0
  113. package/dist/internal/upload/chunkStreamReader.js.map +1 -0
  114. package/dist/internal/upload/chunkStreamReader.test.d.ts +1 -0
  115. package/dist/internal/upload/chunkStreamReader.test.js +75 -0
  116. package/dist/internal/upload/chunkStreamReader.test.js.map +1 -0
  117. package/dist/internal/upload/controller.d.ts +8 -0
  118. package/dist/internal/upload/controller.js +25 -0
  119. package/dist/internal/upload/controller.js.map +1 -0
  120. package/dist/internal/upload/digests.d.ts +8 -0
  121. package/dist/internal/upload/digests.js +22 -0
  122. package/dist/internal/upload/digests.js.map +1 -0
  123. package/dist/internal/upload/fileUploader.d.ts +49 -53
  124. package/dist/internal/upload/fileUploader.js +91 -395
  125. package/dist/internal/upload/fileUploader.js.map +1 -1
  126. package/dist/internal/upload/fileUploader.test.js +38 -292
  127. package/dist/internal/upload/fileUploader.test.js.map +1 -1
  128. package/dist/internal/upload/index.d.ts +3 -3
  129. package/dist/internal/upload/index.js +20 -41
  130. package/dist/internal/upload/index.js.map +1 -1
  131. package/dist/internal/upload/manager.d.ts +1 -1
  132. package/dist/internal/upload/manager.js +16 -19
  133. package/dist/internal/upload/manager.js.map +1 -1
  134. package/dist/internal/upload/manager.test.js +42 -83
  135. package/dist/internal/upload/manager.test.js.map +1 -1
  136. package/dist/internal/upload/queue.d.ts +5 -0
  137. package/dist/internal/upload/queue.js +32 -0
  138. package/dist/internal/upload/queue.js.map +1 -0
  139. package/dist/internal/upload/streamUploader.d.ts +62 -0
  140. package/dist/internal/upload/streamUploader.js +441 -0
  141. package/dist/internal/upload/streamUploader.js.map +1 -0
  142. package/dist/internal/upload/streamUploader.test.d.ts +1 -0
  143. package/dist/internal/upload/streamUploader.test.js +358 -0
  144. package/dist/internal/upload/streamUploader.test.js.map +1 -0
  145. package/dist/internal/utils.d.ts +1 -0
  146. package/dist/internal/utils.js +13 -0
  147. package/dist/internal/utils.js.map +1 -0
  148. package/dist/internal/wait.d.ts +3 -0
  149. package/dist/internal/wait.js +28 -0
  150. package/dist/internal/wait.js.map +1 -0
  151. package/dist/internal/wait.test.d.ts +1 -0
  152. package/dist/internal/wait.test.js +21 -0
  153. package/dist/internal/wait.test.js.map +1 -0
  154. package/dist/protonDriveClient.d.ts +4 -4
  155. package/dist/protonDriveClient.js +1 -1
  156. package/dist/protonDriveClient.js.map +1 -1
  157. package/package.json +2 -2
  158. package/src/errors.ts +10 -4
  159. package/src/interface/index.ts +1 -1
  160. package/src/interface/nodes.ts +11 -0
  161. package/src/interface/upload.ts +53 -3
  162. package/src/internal/apiService/driveTypes.ts +1341 -465
  163. package/src/internal/apiService/errors.ts +3 -2
  164. package/src/internal/apiService/transformers.ts +2 -0
  165. package/src/internal/asyncIteratorMap.test.ts +150 -0
  166. package/src/internal/asyncIteratorMap.ts +64 -0
  167. package/src/internal/nodes/apiService.test.ts +36 -7
  168. package/src/internal/nodes/apiService.ts +19 -7
  169. package/src/internal/nodes/cache.test.ts +1 -0
  170. package/src/internal/nodes/cache.ts +1 -0
  171. package/src/internal/nodes/cryptoService.test.ts +38 -0
  172. package/src/internal/nodes/index.test.ts +3 -1
  173. package/src/internal/nodes/interface.ts +4 -1
  174. package/src/internal/nodes/nodesAccess.test.ts +7 -6
  175. package/src/internal/nodes/nodesAccess.ts +30 -7
  176. package/src/internal/sharing/apiService.ts +24 -2
  177. package/src/internal/upload/fileUploader.test.ts +46 -376
  178. package/src/internal/upload/fileUploader.ts +114 -494
  179. package/src/internal/upload/index.ts +26 -50
  180. package/src/internal/upload/manager.test.ts +45 -92
  181. package/src/internal/upload/manager.ts +30 -32
  182. package/src/internal/upload/streamUploader.test.ts +469 -0
  183. package/src/internal/upload/streamUploader.ts +552 -0
  184. package/src/protonDriveClient.ts +5 -4
@@ -3,12 +3,11 @@ import { DriveAPIService } from "../apiService";
3
3
  import { DriveCrypto } from "../../crypto";
4
4
  import { UploadAPIService } from "./apiService";
5
5
  import { UploadCryptoService } from "./cryptoService";
6
- import { UploadQueue } from "./queue";
6
+ import { FileUploader, FileRevisionUploader } from "./fileUploader";
7
7
  import { NodesService, NodesEvents, SharesService } from "./interface";
8
- import { Fileuploader } from "./fileUploader";
9
- import { UploadTelemetry } from "./telemetry";
10
8
  import { UploadManager } from "./manager";
11
- import { BlockVerifier } from "./blockVerifier";
9
+ import { UploadQueue } from "./queue";
10
+ import { UploadTelemetry } from "./telemetry";
12
11
 
13
12
  /**
14
13
  * Provides facade for the upload module.
@@ -33,85 +32,62 @@ export function initUploadModule(
33
32
 
34
33
  const queue = new UploadQueue();
35
34
 
35
+ /**
36
+ * Returns a FileUploader instance that can be used to upload a file to
37
+ * a parent folder.
38
+ *
39
+ * This operation does not call the API, it only returns a FileUploader
40
+ * instance when the upload queue has capacity.
41
+ */
36
42
  async function getFileUploader(
37
43
  parentFolderUid: string,
38
44
  name: string,
39
45
  metadata: UploadMetadata,
40
46
  signal?: AbortSignal,
41
- ): Promise<Fileuploader> {
47
+ ): Promise<FileUploader> {
42
48
  await queue.waitForCapacity(signal);
43
49
 
44
- let revisionDraft, blockVerifier;
45
- try {
46
- revisionDraft = await manager.createDraftNode(parentFolderUid, name, metadata);
47
-
48
- blockVerifier = new BlockVerifier(api, cryptoService, revisionDraft.nodeKeys.key, revisionDraft.nodeRevisionUid);
49
- await blockVerifier.loadVerificationData();
50
- } catch (error: unknown) {
51
- queue.releaseCapacity();
52
- if (revisionDraft) {
53
- await manager.deleteDraftNode(revisionDraft.nodeUid);
54
- }
55
- void uploadTelemetry.uploadInitFailed(parentFolderUid, error, metadata.expectedSize);
56
- throw error;
57
- }
58
-
59
- const onFinish = async (failure: boolean) => {
50
+ const onFinish = () => {
60
51
  queue.releaseCapacity();
61
- if (failure) {
62
- await manager.deleteDraftNode(revisionDraft.nodeUid);
63
- }
64
52
  }
65
53
 
66
- return new Fileuploader(
54
+ return new FileUploader(
67
55
  uploadTelemetry,
68
56
  api,
69
57
  cryptoService,
70
58
  manager,
71
- blockVerifier,
72
- revisionDraft,
59
+ parentFolderUid,
60
+ name,
73
61
  metadata,
74
62
  onFinish,
75
63
  signal,
76
64
  );
77
65
  }
78
66
 
67
+ /**
68
+ * Returns a FileUploader instance that can be used to upload a new
69
+ * revision of a file.
70
+ *
71
+ * This operation does not call the API, it only returns a
72
+ * FileRevisionUploader instance when the upload queue has capacity.
73
+ */
79
74
  async function getFileRevisionUploader(
80
75
  nodeUid: string,
81
76
  metadata: UploadMetadata,
82
77
  signal?: AbortSignal,
83
- ): Promise<Fileuploader> {
78
+ ): Promise<FileRevisionUploader> {
84
79
  await queue.waitForCapacity(signal);
85
80
 
86
- let revisionDraft, blockVerifier;
87
- try {
88
- revisionDraft = await manager.createDraftRevision(nodeUid, metadata);
89
-
90
- blockVerifier = new BlockVerifier(api, cryptoService, revisionDraft.nodeKeys.key, revisionDraft.nodeRevisionUid);
91
- await blockVerifier.loadVerificationData();
92
- } catch (error: unknown) {
93
- queue.releaseCapacity();
94
- if (revisionDraft) {
95
- await manager.deleteDraftRevision(revisionDraft.nodeRevisionUid);
96
- }
97
- void uploadTelemetry.uploadInitFailed(nodeUid, error, metadata.expectedSize);
98
- throw error;
99
- }
100
-
101
- const onFinish = async (failure: boolean) => {
81
+ const onFinish = () => {
102
82
  queue.releaseCapacity();
103
- if (failure) {
104
- await manager.deleteDraftNode(revisionDraft.nodeUid);
105
- }
106
83
  }
107
84
 
108
- return new Fileuploader(
85
+ return new FileRevisionUploader(
109
86
  uploadTelemetry,
110
87
  api,
111
88
  cryptoService,
112
89
  manager,
113
- blockVerifier,
114
- revisionDraft,
90
+ nodeUid,
115
91
  metadata,
116
92
  onFinish,
117
93
  signal,
@@ -135,14 +135,18 @@ describe("UploadManager", () => {
135
135
  armoredContentKeyPacketSignature: "newNode:armoredContentKeyPacketSignature",
136
136
  signatureEmail: "signatureEmail",
137
137
  });
138
- expect(apiService.checkAvailableHashes).not.toHaveBeenCalled();
139
138
  });
140
139
 
141
- it("should handle existing draft by deleting and trying again", async () => {
142
- let hashChecked = false;
140
+ it("should delete existing draft and trying again", async () => {
141
+ let firstCall = true;
143
142
  apiService.createDraft = jest.fn().mockImplementation(() => {
144
- if (!hashChecked) {
145
- throw new ValidationError("Draft already exists", ErrorCode.ALREADY_EXISTS);
143
+ if (firstCall) {
144
+ firstCall = false;
145
+ throw new ValidationError("Draft already exists", ErrorCode.ALREADY_EXISTS, {
146
+ ConflictLinkID: "existingLinkId",
147
+ ConflictDraftRevisionID: "existingDraftRevisionId",
148
+ ConflictDraftClientUID: "existingDraftClientUid",
149
+ });
146
150
  }
147
151
  return {
148
152
  nodeUid: "newNode:nodeUid",
@@ -150,26 +154,8 @@ describe("UploadManager", () => {
150
154
  };
151
155
  });
152
156
 
153
- apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
154
- if (!hashChecked) {
155
- hashChecked = true;
156
- return {
157
- availalbleHashes: ["name1Hash"],
158
- pendingHashes: [{
159
- hash: "newNode:hash",
160
- nodeUid: "nodeUidToDelete"
161
- }],
162
- }
163
- }
164
- return {
165
- availalbleHashes: ["name1Hash"],
166
- pendingHashes: [],
167
- }
168
- });
157
+ const result = await manager.createDraftNode("volumeId~parentUid", "name", {} as UploadMetadata);
169
158
 
170
- const result = await manager.createDraftNode("parentUid", "name", {} as UploadMetadata);
171
-
172
- expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(1);
173
159
  expect(apiService.deleteDraft).toHaveBeenCalledTimes(1);
174
160
  expect(result).toEqual({
175
161
  nodeUid: "newNode:nodeUid",
@@ -182,20 +168,25 @@ describe("UploadManager", () => {
182
168
  },
183
169
  },
184
170
  newNodeInfo: {
185
- parentUid: "parentUid",
171
+ parentUid: "volumeId~parentUid",
186
172
  name: "name",
187
173
  encryptedName: "newNode:encryptedName",
188
174
  hash: "newNode:hash",
189
175
  },
190
176
  });
191
- expect(apiService.deleteDraft).toHaveBeenCalledWith("nodeUidToDelete");
177
+ expect(apiService.deleteDraft).toHaveBeenCalledWith("volumeId~existingLinkId");
192
178
  });
193
179
 
194
180
  it("should handle error when deleting existing draft", async () => {
195
- let hashChecked = false;
181
+ let firstCall = true;
196
182
  apiService.createDraft = jest.fn().mockImplementation(() => {
197
- if (!hashChecked) {
198
- throw new ValidationError("Draft already exists", ErrorCode.ALREADY_EXISTS);
183
+ if (firstCall) {
184
+ firstCall = false;
185
+ throw new ValidationError("Draft already exists", ErrorCode.ALREADY_EXISTS, {
186
+ ConflictLinkID: "existingLinkId",
187
+ ConflictDraftRevisionID: "existingDraftRevisionId",
188
+ ConflictDraftClientUID: "existingDraftClientUid",
189
+ });
199
190
  }
200
191
  return {
201
192
  nodeUid: "newNode:nodeUid",
@@ -206,70 +197,38 @@ describe("UploadManager", () => {
206
197
  throw new Error("Failed to delete draft");
207
198
  });
208
199
 
209
- apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
210
- if (!hashChecked) {
211
- hashChecked = true;
212
- return {
213
- availalbleHashes: ["name1Hash"],
214
- pendingHashes: [{
215
- hash: "newNode:hash",
216
- nodeUid: "nodeUidToDelete"
217
- }],
218
- }
219
- }
220
- return {
221
- availalbleHashes: ["name1Hash"],
222
- pendingHashes: [],
223
- }
224
- });
225
-
226
- const result = manager.createDraftNode("parentUid", "name", {} as UploadMetadata);
227
-
228
- await expect(result).rejects.toThrow("Draft already exists");
229
- expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(1);
230
- expect(apiService.deleteDraft).toHaveBeenCalledTimes(1);
231
- });
232
-
233
- it("should handle existing name by providing available name", async () => {
234
- let count = 0;
235
- apiService.createDraft = jest.fn().mockImplementation(() => {
236
- if (count === 0) {
237
- count++;
238
- throw new ValidationError("Draft already exists", ErrorCode.ALREADY_EXISTS);
239
- }
240
- return {
241
- nodeUid: "newNode:nodeUid",
242
- nodeRevisionUid: "newNode:nodeRevisionUid",
243
- };
244
- });
245
-
246
- const result = manager.createDraftNode("parentUid", "name", {} as UploadMetadata);
247
-
248
- await expect(result).rejects.toThrow("Draft already exists");
249
- expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(1);
200
+ const result = manager.createDraftNode("volumeId~parentUid", "name", {} as UploadMetadata);
250
201
 
251
202
  try {
252
203
  await result;
253
204
  } catch (error: any) {
254
- expect(error.availableName).toBe("name1");
205
+ expect(error.message).toBe("Draft already exists");
206
+ expect(error.existingNodeUid).toBe("volumeId~existingLinkId");
255
207
  }
208
+ expect(apiService.deleteDraft).toHaveBeenCalledTimes(1);
256
209
  });
210
+ });
257
211
 
258
- it("should handle existing name by providing available name when there is too many conflicts", async () => {
259
- let hashChecked = false;
260
- apiService.createDraft = jest.fn().mockImplementation(() => {
261
- if (!hashChecked) {
262
- throw new ValidationError("Draft already exists", ErrorCode.ALREADY_EXISTS);
263
- }
212
+ describe("findAvailableName", () => {
213
+ it("should find available name", async () => {
214
+ apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
264
215
  return {
265
- nodeUid: "newNode:nodeUid",
266
- nodeRevisionUid: "newNode:nodeRevisionUid",
267
- };
216
+ availalbleHashes: ["name3Hash"],
217
+ pendingHashes: [],
218
+ }
268
219
  });
269
220
 
221
+ const result = await manager.findAvailableName("parentUid", "name");
222
+ expect(result).toBe("name3");
223
+ expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(1);
224
+ expect(apiService.checkAvailableHashes).toHaveBeenCalledWith("parentUid", ["name1Hash", "name2Hash", "name3Hash"]);
225
+ });
226
+
227
+ it("should find available name with multiple pages", async () => {
228
+ let firstCall = false;
270
229
  apiService.checkAvailableHashes = jest.fn().mockImplementation(() => {
271
- if (!hashChecked) {
272
- hashChecked = true;
230
+ if (!firstCall) {
231
+ firstCall = true;
273
232
  return {
274
233
  // First page has no available hashes
275
234
  availalbleHashes: [],
@@ -282,16 +241,10 @@ describe("UploadManager", () => {
282
241
  }
283
242
  });
284
243
 
285
- const result = manager.createDraftNode("parentUid", "name", {} as UploadMetadata);
286
-
287
- await expect(result).rejects.toThrow("Draft already exists");
244
+ const result = await manager.findAvailableName("parentUid", "name");
245
+ expect(result).toBe("name3");
288
246
  expect(apiService.checkAvailableHashes).toHaveBeenCalledTimes(2);
289
-
290
- try {
291
- await result;
292
- } catch (error: any) {
293
- expect(error.availableName).toBe("name3");
294
- }
247
+ expect(apiService.checkAvailableHashes).toHaveBeenCalledWith("parentUid", ["name1Hash", "name2Hash", "name3Hash"]);
295
248
  });
296
249
  });
297
250
 
@@ -300,7 +253,7 @@ describe("UploadManager", () => {
300
253
  nodeUid: "newNode:nodeUid",
301
254
  nodeRevisionUid: "newNode:nodeRevisionUid",
302
255
  nodeKeys: {
303
- key: {_idx: 32321},
256
+ key: { _idx: 32321 },
304
257
  contentKeyPacketSessionKey: "newNode:contentKeyPacketSessionKey",
305
258
  signatureAddress: {
306
259
  email: "signatureEmail",
@@ -7,6 +7,7 @@ import { DecryptedNode, generateFileExtendedAttributes } from "../nodes";
7
7
  import { UploadAPIService } from "./apiService";
8
8
  import { UploadCryptoService } from "./cryptoService";
9
9
  import { NodeRevisionDraft, NodesService, NodesEvents, NodeCrypto } from "./interface";
10
+ import { makeNodeUid, splitNodeUid } from "../uids";
10
11
 
11
12
  /**
12
13
  * UploadManager is responsible for creating and deleting draft nodes
@@ -95,23 +96,26 @@ export class UploadManager {
95
96
  if (error instanceof ValidationError) {
96
97
  if (error.code === ErrorCode.ALREADY_EXISTS) {
97
98
  this.logger.info(`Node with given name already exists`);
98
- const availableName = await this.findAvailableName(
99
- parentFolderUid,
100
- parentHashKey,
101
- name,
102
- generatedNodeCrypto.encryptedNode.hash,
103
- );
99
+
100
+ const typedDetails = error.details as {
101
+ ConflictLinkID: string,
102
+ ConflictRevisionID?: string,
103
+ ConflictDraftRevisionID?: string,
104
+ ConflictDraftClientUID?: string,
105
+ } | undefined;
104
106
 
105
107
  // If there is existing draft created by this client,
106
108
  // automatically delete it and try to create a new one
107
109
  // with the same name again.
108
- if (availableName.existingDraftNodeUid) {
110
+ if (typedDetails?.ConflictDraftRevisionID) {
111
+ const existingDraftNodeUid = makeNodeUid(splitNodeUid(parentFolderUid).volumeId, typedDetails.ConflictLinkID);
112
+
109
113
  let deleteFailed = false;
110
114
  try {
111
- this.logger.warn(`Deleting existing draft node ${availableName.existingDraftNodeUid}`);
112
- await this.apiService.deleteDraft(availableName.existingDraftNodeUid);
115
+ this.logger.warn(`Deleting existing draft node ${existingDraftNodeUid}`);
116
+ await this.apiService.deleteDraft(existingDraftNodeUid);
113
117
  } catch (deleteDraftError: unknown) {
114
- // Do not throw, let return the next available name to the client.
118
+ // Do not throw, let throw the conflict error.
115
119
  deleteFailed = true;
116
120
  this.logger.error('Failed to delete existing draft node', deleteDraftError);
117
121
  }
@@ -120,12 +124,14 @@ export class UploadManager {
120
124
  }
121
125
  }
122
126
 
127
+ const existingNodeUid = typedDetails ? makeNodeUid(splitNodeUid(parentFolderUid).volumeId, typedDetails.ConflictLinkID) : undefined;
128
+
123
129
  // If there is existing node, return special error
124
130
  // that includes the available name the client can use.
125
131
  throw new NodeAlreadyExistsValidationError(
126
132
  error.message,
127
133
  error.code,
128
- availableName.availableName,
134
+ existingNodeUid,
129
135
  );
130
136
  }
131
137
  }
@@ -133,10 +139,12 @@ export class UploadManager {
133
139
  }
134
140
  }
135
141
 
136
- private async findAvailableName(parentFolderUid: string, parentHashKey: Uint8Array, name: string, nameHash: string): Promise<{
137
- availableName: string,
138
- existingDraftNodeUid?: string,
139
- }> {
142
+ async findAvailableName(parentFolderUid: string, name: string): Promise<string> {
143
+ const { hashKey: parentHashKey } = await this.nodesService.getNodeKeys(parentFolderUid);
144
+ if (!parentHashKey) {
145
+ throw new ValidationError(c('Error').t`Creating files in non-folders is not allowed`);
146
+ }
147
+
140
148
  const [namePart, extension] = splitExtension(name);
141
149
 
142
150
  const batchSize = 10;
@@ -149,14 +157,9 @@ export class UploadManager {
149
157
 
150
158
  const hashesToCheck = await this.cryptoService.generateNameHashes(parentHashKey, namesToCheck);
151
159
 
152
- const { pendingHashes, availalbleHashes } = await this.apiService.checkAvailableHashes(
160
+ const { availalbleHashes } = await this.apiService.checkAvailableHashes(
153
161
  parentFolderUid,
154
- [
155
- ...hashesToCheck.map(({ hash }) => hash),
156
- // Adding the current name hash to get the existing draft
157
- // node UID if it exists.
158
- ...startIndex ? [nameHash] : [],
159
- ],
162
+ hashesToCheck.map(({ hash }) => hash),
160
163
  );
161
164
 
162
165
  if (!availalbleHashes.length) {
@@ -169,12 +172,7 @@ export class UploadManager {
169
172
  throw Error('Backend returned unexpected hash');
170
173
  }
171
174
 
172
- // FIXME: use client UID to ensure its own pending draft
173
- const ownPendingHash = pendingHashes.find(({ hash }) => hash === nameHash);
174
- return {
175
- availableName: availableHash.name,
176
- existingDraftNodeUid: ownPendingHash?.nodeUid,
177
- }
175
+ return availableHash.name;
178
176
  }
179
177
  }
180
178
 
@@ -261,7 +259,7 @@ export class UploadManager {
261
259
  // Internal metadata
262
260
  hash: nodeRevisionDraft.newNodeInfo.hash,
263
261
  encryptedName: nodeRevisionDraft.newNodeInfo.encryptedName,
264
-
262
+
265
263
  // Basic node metadata
266
264
  uid: nodeRevisionDraft.nodeUid,
267
265
  parentUid: nodeRevisionDraft.newNodeInfo.parentUid,
@@ -269,11 +267,11 @@ export class UploadManager {
269
267
  mediaType: metadata.mediaType,
270
268
  creationTime: new Date(),
271
269
  totalStorageSize: encryptedSize,
272
-
270
+
273
271
  // Share node metadata
274
272
  isShared: false,
275
273
  directMemberRole: MemberRole.Inherited,
276
-
274
+
277
275
  // Decrypted metadata
278
276
  isStale: false,
279
277
  keyAuthor: resultOk(nodeRevisionDraft.nodeKeys.signatureAddress.email),
@@ -297,7 +295,7 @@ export class UploadManager {
297
295
  */
298
296
  function splitExtension(filename = ''): [string, string] {
299
297
  const endIdx = filename.lastIndexOf('.');
300
- if (endIdx === -1 || endIdx === filename.length-1) {
298
+ if (endIdx === -1 || endIdx === filename.length - 1) {
301
299
  return [filename, ''];
302
300
  }
303
301
  return [filename.slice(0, endIdx), filename.slice(endIdx + 1)];