@arke-institute/sdk 2.0.0 → 2.2.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.
- package/README.md +108 -4
- package/dist/{index-BrXke2kI.d.ts → crypto-7c990p-j.d.ts} +200 -19
- package/dist/{index-FHcLPBSV.d.cts → crypto-El5Z3bNI.d.cts} +200 -19
- package/dist/generated/index.d.cts +680 -76
- package/dist/generated/index.d.ts +680 -76
- package/dist/index.cjs +638 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +636 -11
- package/dist/index.js.map +1 -1
- package/dist/operations/index.cjs +774 -10
- package/dist/operations/index.cjs.map +1 -1
- package/dist/operations/index.d.cts +171 -1
- package/dist/operations/index.d.ts +171 -1
- package/dist/operations/index.js +755 -9
- package/dist/operations/index.js.map +1 -1
- package/openapi/spec.json +9031 -0
- package/openapi/version.json +7 -0
- package/package.json +12 -4
package/dist/index.js
CHANGED
|
@@ -9,9 +9,9 @@ var DEFAULT_CONFIG = {
|
|
|
9
9
|
|
|
10
10
|
// src/client/errors.ts
|
|
11
11
|
var ArkeError = class extends Error {
|
|
12
|
-
constructor(message,
|
|
12
|
+
constructor(message, code2, status, details) {
|
|
13
13
|
super(message);
|
|
14
|
-
this.code =
|
|
14
|
+
this.code = code2;
|
|
15
15
|
this.status = status;
|
|
16
16
|
this.details = details;
|
|
17
17
|
this.name = "ArkeError";
|
|
@@ -88,6 +88,15 @@ function parseApiError(status, body) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// src/client/ArkeClient.ts
|
|
91
|
+
function isApiKey(token) {
|
|
92
|
+
return token.startsWith("ak_") || token.startsWith("uk_");
|
|
93
|
+
}
|
|
94
|
+
function getAuthorizationHeader(token) {
|
|
95
|
+
if (isApiKey(token)) {
|
|
96
|
+
return `ApiKey ${token}`;
|
|
97
|
+
}
|
|
98
|
+
return `Bearer ${token}`;
|
|
99
|
+
}
|
|
91
100
|
var ArkeClient = class {
|
|
92
101
|
constructor(config = {}) {
|
|
93
102
|
this.config = {
|
|
@@ -102,7 +111,7 @@ var ArkeClient = class {
|
|
|
102
111
|
...this.config.headers
|
|
103
112
|
};
|
|
104
113
|
if (this.config.authToken) {
|
|
105
|
-
headers["Authorization"] =
|
|
114
|
+
headers["Authorization"] = getAuthorizationHeader(this.config.authToken);
|
|
106
115
|
}
|
|
107
116
|
if (this.config.network === "test") {
|
|
108
117
|
headers["X-Arke-Network"] = "test";
|
|
@@ -150,6 +159,603 @@ function createArkeClient(config) {
|
|
|
150
159
|
return new ArkeClient(config);
|
|
151
160
|
}
|
|
152
161
|
|
|
162
|
+
// src/operations/upload/cid.ts
|
|
163
|
+
import { CID } from "multiformats/cid";
|
|
164
|
+
import { sha256 } from "multiformats/hashes/sha2";
|
|
165
|
+
import * as raw from "multiformats/codecs/raw";
|
|
166
|
+
async function computeCid(data) {
|
|
167
|
+
let bytes;
|
|
168
|
+
if (data instanceof Blob) {
|
|
169
|
+
const buffer = await data.arrayBuffer();
|
|
170
|
+
bytes = new Uint8Array(buffer);
|
|
171
|
+
} else if (data instanceof ArrayBuffer) {
|
|
172
|
+
bytes = new Uint8Array(data);
|
|
173
|
+
} else {
|
|
174
|
+
bytes = data;
|
|
175
|
+
}
|
|
176
|
+
const hash = await sha256.digest(bytes);
|
|
177
|
+
const cid = CID.create(1, raw.code, hash);
|
|
178
|
+
return cid.toString();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/operations/upload/engine.ts
|
|
182
|
+
var PHASE_COUNT = 4;
|
|
183
|
+
var PHASE_INDEX = {
|
|
184
|
+
"computing-cids": 0,
|
|
185
|
+
"creating": 1,
|
|
186
|
+
"backlinking": 2,
|
|
187
|
+
"uploading": 3,
|
|
188
|
+
"complete": 4,
|
|
189
|
+
"error": -1
|
|
190
|
+
};
|
|
191
|
+
async function parallelLimit(items, concurrency, fn) {
|
|
192
|
+
const results = [];
|
|
193
|
+
let index = 0;
|
|
194
|
+
async function worker() {
|
|
195
|
+
while (index < items.length) {
|
|
196
|
+
const currentIndex = index++;
|
|
197
|
+
const item = items[currentIndex];
|
|
198
|
+
results[currentIndex] = await fn(item, currentIndex);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
202
|
+
await Promise.all(workers);
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
var TARGET_BYTES_IN_FLIGHT = 200 * 1024 * 1024;
|
|
206
|
+
var BytePool = class {
|
|
207
|
+
constructor(targetBytes = TARGET_BYTES_IN_FLIGHT) {
|
|
208
|
+
this.targetBytes = targetBytes;
|
|
209
|
+
this.bytesInFlight = 0;
|
|
210
|
+
this.waitQueue = [];
|
|
211
|
+
}
|
|
212
|
+
async run(size, fn) {
|
|
213
|
+
while (this.bytesInFlight > 0 && this.bytesInFlight + size > this.targetBytes) {
|
|
214
|
+
await new Promise((resolve) => this.waitQueue.push(resolve));
|
|
215
|
+
}
|
|
216
|
+
this.bytesInFlight += size;
|
|
217
|
+
try {
|
|
218
|
+
return await fn();
|
|
219
|
+
} finally {
|
|
220
|
+
this.bytesInFlight -= size;
|
|
221
|
+
const next = this.waitQueue.shift();
|
|
222
|
+
if (next) next();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function getParentPath(relativePath) {
|
|
227
|
+
const lastSlash = relativePath.lastIndexOf("/");
|
|
228
|
+
if (lastSlash === -1) return null;
|
|
229
|
+
return relativePath.slice(0, lastSlash);
|
|
230
|
+
}
|
|
231
|
+
function groupFoldersByDepth(folders) {
|
|
232
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const folder of folders) {
|
|
234
|
+
const depth = folder.relativePath.split("/").length - 1;
|
|
235
|
+
if (!byDepth.has(depth)) byDepth.set(depth, []);
|
|
236
|
+
byDepth.get(depth).push(folder);
|
|
237
|
+
}
|
|
238
|
+
return byDepth;
|
|
239
|
+
}
|
|
240
|
+
async function uploadTree(client, tree, options) {
|
|
241
|
+
const { target, onProgress, concurrency = 10, continueOnError = false, note } = options;
|
|
242
|
+
const errors = [];
|
|
243
|
+
const createdFolders = [];
|
|
244
|
+
const createdFiles = [];
|
|
245
|
+
const foldersByPath = /* @__PURE__ */ new Map();
|
|
246
|
+
const totalEntities = tree.files.length + tree.folders.length;
|
|
247
|
+
const totalBytes = tree.files.reduce((sum, f) => sum + f.size, 0);
|
|
248
|
+
let completedEntities = 0;
|
|
249
|
+
let bytesUploaded = 0;
|
|
250
|
+
const reportProgress = (progress) => {
|
|
251
|
+
if (onProgress) {
|
|
252
|
+
const phase = progress.phase || "computing-cids";
|
|
253
|
+
const phaseIndex = PHASE_INDEX[phase] ?? -1;
|
|
254
|
+
let phasePercent = 0;
|
|
255
|
+
if (phase === "computing-cids") {
|
|
256
|
+
const done = progress.completedEntities ?? completedEntities;
|
|
257
|
+
phasePercent = tree.files.length > 0 ? Math.round(done / tree.files.length * 100) : 100;
|
|
258
|
+
} else if (phase === "creating") {
|
|
259
|
+
const done = progress.completedEntities ?? completedEntities;
|
|
260
|
+
phasePercent = totalEntities > 0 ? Math.round(done / totalEntities * 100) : 100;
|
|
261
|
+
} else if (phase === "backlinking") {
|
|
262
|
+
const done = progress.completedParents ?? 0;
|
|
263
|
+
const total = progress.totalParents ?? 0;
|
|
264
|
+
phasePercent = total > 0 ? Math.round(done / total * 100) : 100;
|
|
265
|
+
} else if (phase === "uploading") {
|
|
266
|
+
const done = progress.bytesUploaded ?? bytesUploaded;
|
|
267
|
+
phasePercent = totalBytes > 0 ? Math.round(done / totalBytes * 100) : 100;
|
|
268
|
+
} else if (phase === "complete") {
|
|
269
|
+
phasePercent = 100;
|
|
270
|
+
}
|
|
271
|
+
onProgress({
|
|
272
|
+
phase,
|
|
273
|
+
phaseIndex,
|
|
274
|
+
phaseCount: PHASE_COUNT,
|
|
275
|
+
phasePercent,
|
|
276
|
+
totalEntities,
|
|
277
|
+
completedEntities,
|
|
278
|
+
totalParents: 0,
|
|
279
|
+
completedParents: 0,
|
|
280
|
+
totalBytes,
|
|
281
|
+
bytesUploaded,
|
|
282
|
+
...progress
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
let collectionId;
|
|
288
|
+
let collectionCid;
|
|
289
|
+
let collectionCreated = false;
|
|
290
|
+
if (target.createCollection) {
|
|
291
|
+
const collectionBody = {
|
|
292
|
+
label: target.createCollection.label,
|
|
293
|
+
description: target.createCollection.description,
|
|
294
|
+
roles: target.createCollection.roles,
|
|
295
|
+
note
|
|
296
|
+
};
|
|
297
|
+
const { data, error } = await client.api.POST("/collections", {
|
|
298
|
+
body: collectionBody
|
|
299
|
+
});
|
|
300
|
+
if (error || !data) {
|
|
301
|
+
throw new Error(`Failed to create collection: ${JSON.stringify(error)}`);
|
|
302
|
+
}
|
|
303
|
+
collectionId = data.id;
|
|
304
|
+
collectionCid = data.cid;
|
|
305
|
+
collectionCreated = true;
|
|
306
|
+
} else if (target.collectionId) {
|
|
307
|
+
collectionId = target.collectionId;
|
|
308
|
+
const { data, error } = await client.api.GET("/collections/{id}", {
|
|
309
|
+
params: { path: { id: collectionId } }
|
|
310
|
+
});
|
|
311
|
+
if (error || !data) {
|
|
312
|
+
throw new Error(`Failed to fetch collection: ${JSON.stringify(error)}`);
|
|
313
|
+
}
|
|
314
|
+
collectionCid = data.cid;
|
|
315
|
+
} else {
|
|
316
|
+
throw new Error("Must provide either collectionId or createCollection in target");
|
|
317
|
+
}
|
|
318
|
+
const rootParentId = target.parentId ?? collectionId;
|
|
319
|
+
reportProgress({ phase: "computing-cids", completedEntities: 0 });
|
|
320
|
+
const preparedFiles = [];
|
|
321
|
+
let cidProgress = 0;
|
|
322
|
+
await parallelLimit(tree.files, Math.max(concurrency, 20), async (file) => {
|
|
323
|
+
try {
|
|
324
|
+
const data = await file.getData();
|
|
325
|
+
const cid = await computeCid(data);
|
|
326
|
+
preparedFiles.push({ ...file, cid });
|
|
327
|
+
cidProgress++;
|
|
328
|
+
reportProgress({
|
|
329
|
+
phase: "computing-cids",
|
|
330
|
+
completedEntities: cidProgress,
|
|
331
|
+
currentItem: file.relativePath
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
335
|
+
if (continueOnError) {
|
|
336
|
+
errors.push({ path: file.relativePath, error: `CID computation failed: ${errorMsg}` });
|
|
337
|
+
} else {
|
|
338
|
+
throw new Error(`Failed to compute CID for ${file.relativePath}: ${errorMsg}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
reportProgress({ phase: "creating", completedEntities: 0 });
|
|
343
|
+
const foldersByDepth = groupFoldersByDepth(tree.folders);
|
|
344
|
+
const sortedDepths = [...foldersByDepth.keys()].sort((a, b) => a - b);
|
|
345
|
+
for (const depth of sortedDepths) {
|
|
346
|
+
const foldersAtDepth = foldersByDepth.get(depth);
|
|
347
|
+
await Promise.all(
|
|
348
|
+
foldersAtDepth.map(async (folder) => {
|
|
349
|
+
try {
|
|
350
|
+
const parentPath = getParentPath(folder.relativePath);
|
|
351
|
+
const parentId = parentPath ? foldersByPath.get(parentPath).id : rootParentId;
|
|
352
|
+
const parentType = parentPath ? "folder" : parentId === collectionId ? "collection" : "folder";
|
|
353
|
+
const folderBody = {
|
|
354
|
+
label: folder.name,
|
|
355
|
+
collection: collectionId,
|
|
356
|
+
note,
|
|
357
|
+
relationships: [{ predicate: "in", peer: parentId, peer_type: parentType }]
|
|
358
|
+
};
|
|
359
|
+
const { data, error } = await client.api.POST("/folders", {
|
|
360
|
+
body: folderBody
|
|
361
|
+
});
|
|
362
|
+
if (error || !data) {
|
|
363
|
+
throw new Error(JSON.stringify(error));
|
|
364
|
+
}
|
|
365
|
+
foldersByPath.set(folder.relativePath, { id: data.id, cid: data.cid });
|
|
366
|
+
createdFolders.push({
|
|
367
|
+
name: folder.name,
|
|
368
|
+
relativePath: folder.relativePath,
|
|
369
|
+
id: data.id,
|
|
370
|
+
entityCid: data.cid
|
|
371
|
+
});
|
|
372
|
+
completedEntities++;
|
|
373
|
+
reportProgress({
|
|
374
|
+
phase: "creating",
|
|
375
|
+
completedEntities,
|
|
376
|
+
currentItem: folder.relativePath
|
|
377
|
+
});
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
380
|
+
if (continueOnError) {
|
|
381
|
+
errors.push({ path: folder.relativePath, error: `Folder creation failed: ${errorMsg}` });
|
|
382
|
+
completedEntities++;
|
|
383
|
+
} else {
|
|
384
|
+
throw new Error(`Failed to create folder ${folder.relativePath}: ${errorMsg}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const FILE_CREATION_CONCURRENCY = 50;
|
|
391
|
+
await parallelLimit(preparedFiles, FILE_CREATION_CONCURRENCY, async (file) => {
|
|
392
|
+
try {
|
|
393
|
+
const parentPath = getParentPath(file.relativePath);
|
|
394
|
+
const parentId = parentPath ? foldersByPath.get(parentPath).id : rootParentId;
|
|
395
|
+
const parentType = parentPath ? "folder" : parentId === collectionId ? "collection" : "folder";
|
|
396
|
+
const fileBody = {
|
|
397
|
+
key: file.cid,
|
|
398
|
+
filename: file.name,
|
|
399
|
+
content_type: file.mimeType,
|
|
400
|
+
size: file.size,
|
|
401
|
+
cid: file.cid,
|
|
402
|
+
collection: collectionId,
|
|
403
|
+
relationships: [{ predicate: "in", peer: parentId, peer_type: parentType }]
|
|
404
|
+
};
|
|
405
|
+
const { data, error } = await client.api.POST("/files", {
|
|
406
|
+
body: fileBody
|
|
407
|
+
});
|
|
408
|
+
if (error || !data) {
|
|
409
|
+
throw new Error(`Entity creation failed: ${JSON.stringify(error)}`);
|
|
410
|
+
}
|
|
411
|
+
createdFiles.push({
|
|
412
|
+
...file,
|
|
413
|
+
id: data.id,
|
|
414
|
+
entityCid: data.cid,
|
|
415
|
+
uploadUrl: data.upload_url,
|
|
416
|
+
uploadExpiresAt: data.upload_expires_at
|
|
417
|
+
});
|
|
418
|
+
completedEntities++;
|
|
419
|
+
reportProgress({
|
|
420
|
+
phase: "creating",
|
|
421
|
+
completedEntities,
|
|
422
|
+
currentItem: file.relativePath
|
|
423
|
+
});
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
426
|
+
if (continueOnError) {
|
|
427
|
+
errors.push({ path: file.relativePath, error: errorMsg });
|
|
428
|
+
completedEntities++;
|
|
429
|
+
} else {
|
|
430
|
+
throw new Error(`Failed to create file ${file.relativePath}: ${errorMsg}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const folder of createdFolders) {
|
|
436
|
+
const parentPath = getParentPath(folder.relativePath);
|
|
437
|
+
const parentId = parentPath ? foldersByPath.get(parentPath).id : rootParentId;
|
|
438
|
+
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);
|
|
439
|
+
childrenByParent.get(parentId).push({ id: folder.id, type: "folder" });
|
|
440
|
+
}
|
|
441
|
+
for (const file of createdFiles) {
|
|
442
|
+
const parentPath = getParentPath(file.relativePath);
|
|
443
|
+
const parentId = parentPath ? foldersByPath.get(parentPath).id : rootParentId;
|
|
444
|
+
if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);
|
|
445
|
+
childrenByParent.get(parentId).push({ id: file.id, type: "file" });
|
|
446
|
+
}
|
|
447
|
+
const totalParents = childrenByParent.size;
|
|
448
|
+
let completedParents = 0;
|
|
449
|
+
reportProgress({ phase: "backlinking", totalParents, completedParents: 0 });
|
|
450
|
+
const parentEntries = [...childrenByParent.entries()];
|
|
451
|
+
await parallelLimit(parentEntries, concurrency, async ([parentId, children]) => {
|
|
452
|
+
try {
|
|
453
|
+
const isCollection = parentId === collectionId;
|
|
454
|
+
const relationshipsAdd = children.map((child) => ({
|
|
455
|
+
predicate: "contains",
|
|
456
|
+
peer: child.id,
|
|
457
|
+
peer_type: child.type
|
|
458
|
+
}));
|
|
459
|
+
if (isCollection) {
|
|
460
|
+
const { data: collData, error: getError } = await client.api.GET("/collections/{id}", {
|
|
461
|
+
params: { path: { id: parentId } }
|
|
462
|
+
});
|
|
463
|
+
if (getError || !collData) {
|
|
464
|
+
throw new Error(`Failed to fetch collection: ${JSON.stringify(getError)}`);
|
|
465
|
+
}
|
|
466
|
+
const updateBody = {
|
|
467
|
+
expect_tip: collData.cid,
|
|
468
|
+
relationships_add: relationshipsAdd,
|
|
469
|
+
note: note ? `${note} (backlink)` : "Upload backlink"
|
|
470
|
+
};
|
|
471
|
+
const { error } = await client.api.PUT("/collections/{id}", {
|
|
472
|
+
params: { path: { id: parentId } },
|
|
473
|
+
body: updateBody
|
|
474
|
+
});
|
|
475
|
+
if (error) {
|
|
476
|
+
throw new Error(JSON.stringify(error));
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
const { data: folderData, error: getError } = await client.api.GET("/folders/{id}", {
|
|
480
|
+
params: { path: { id: parentId } }
|
|
481
|
+
});
|
|
482
|
+
if (getError || !folderData) {
|
|
483
|
+
throw new Error(`Failed to fetch folder: ${JSON.stringify(getError)}`);
|
|
484
|
+
}
|
|
485
|
+
const updateBody = {
|
|
486
|
+
expect_tip: folderData.cid,
|
|
487
|
+
relationships_add: relationshipsAdd,
|
|
488
|
+
note: note ? `${note} (backlink)` : "Upload backlink"
|
|
489
|
+
};
|
|
490
|
+
const { error } = await client.api.PUT("/folders/{id}", {
|
|
491
|
+
params: { path: { id: parentId } },
|
|
492
|
+
body: updateBody
|
|
493
|
+
});
|
|
494
|
+
if (error) {
|
|
495
|
+
throw new Error(JSON.stringify(error));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
completedParents++;
|
|
499
|
+
reportProgress({
|
|
500
|
+
phase: "backlinking",
|
|
501
|
+
totalParents,
|
|
502
|
+
completedParents,
|
|
503
|
+
currentItem: `parent:${parentId}`
|
|
504
|
+
});
|
|
505
|
+
} catch (err) {
|
|
506
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
507
|
+
if (continueOnError) {
|
|
508
|
+
errors.push({ path: `parent:${parentId}`, error: `Backlink failed: ${errorMsg}` });
|
|
509
|
+
completedParents++;
|
|
510
|
+
} else {
|
|
511
|
+
throw new Error(`Failed to backlink parent ${parentId}: ${errorMsg}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
reportProgress({ phase: "uploading", bytesUploaded: 0 });
|
|
516
|
+
const pool = new BytePool();
|
|
517
|
+
await Promise.all(
|
|
518
|
+
createdFiles.map(async (file) => {
|
|
519
|
+
await pool.run(file.size, async () => {
|
|
520
|
+
try {
|
|
521
|
+
const fileData = await file.getData();
|
|
522
|
+
let body;
|
|
523
|
+
if (fileData instanceof Blob) {
|
|
524
|
+
body = fileData;
|
|
525
|
+
} else if (fileData instanceof Uint8Array) {
|
|
526
|
+
const arrayBuffer = new ArrayBuffer(fileData.byteLength);
|
|
527
|
+
new Uint8Array(arrayBuffer).set(fileData);
|
|
528
|
+
body = new Blob([arrayBuffer], { type: file.mimeType });
|
|
529
|
+
} else {
|
|
530
|
+
body = new Blob([fileData], { type: file.mimeType });
|
|
531
|
+
}
|
|
532
|
+
const uploadResponse = await fetch(file.uploadUrl, {
|
|
533
|
+
method: "PUT",
|
|
534
|
+
body,
|
|
535
|
+
headers: { "Content-Type": file.mimeType }
|
|
536
|
+
});
|
|
537
|
+
if (!uploadResponse.ok) {
|
|
538
|
+
throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
|
|
539
|
+
}
|
|
540
|
+
let confirmTip = file.entityCid;
|
|
541
|
+
let confirmAttempts = 0;
|
|
542
|
+
const MAX_CONFIRM_ATTEMPTS = 3;
|
|
543
|
+
while (confirmAttempts < MAX_CONFIRM_ATTEMPTS) {
|
|
544
|
+
confirmAttempts++;
|
|
545
|
+
const { error: confirmError } = await client.api.POST("/files/{id}/confirm-upload", {
|
|
546
|
+
params: { path: { id: file.id } },
|
|
547
|
+
body: {
|
|
548
|
+
expect_tip: confirmTip,
|
|
549
|
+
note: note ? `${note} (confirmed)` : "Upload confirmed"
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
if (!confirmError) {
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
const errorStr = JSON.stringify(confirmError);
|
|
556
|
+
if (errorStr.includes("409") || errorStr.includes("CAS") || errorStr.includes("conflict")) {
|
|
557
|
+
const { data: currentFile, error: fetchError } = await client.api.GET("/files/{id}", {
|
|
558
|
+
params: { path: { id: file.id } }
|
|
559
|
+
});
|
|
560
|
+
if (fetchError || !currentFile) {
|
|
561
|
+
throw new Error(`Failed to fetch file for confirm retry: ${JSON.stringify(fetchError)}`);
|
|
562
|
+
}
|
|
563
|
+
confirmTip = currentFile.cid;
|
|
564
|
+
} else {
|
|
565
|
+
throw new Error(`Confirm upload failed: ${errorStr}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (confirmAttempts >= MAX_CONFIRM_ATTEMPTS) {
|
|
569
|
+
throw new Error(`Confirm upload failed after ${MAX_CONFIRM_ATTEMPTS} CAS retries`);
|
|
570
|
+
}
|
|
571
|
+
bytesUploaded += file.size;
|
|
572
|
+
reportProgress({
|
|
573
|
+
phase: "uploading",
|
|
574
|
+
bytesUploaded,
|
|
575
|
+
currentItem: file.relativePath
|
|
576
|
+
});
|
|
577
|
+
} catch (err) {
|
|
578
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
579
|
+
if (continueOnError) {
|
|
580
|
+
errors.push({ path: file.relativePath, error: `Upload failed: ${errorMsg}` });
|
|
581
|
+
} else {
|
|
582
|
+
throw new Error(`Failed to upload ${file.relativePath}: ${errorMsg}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
})
|
|
587
|
+
);
|
|
588
|
+
reportProgress({ phase: "complete", totalParents, completedParents, bytesUploaded });
|
|
589
|
+
const resultFolders = createdFolders.map((f) => ({
|
|
590
|
+
id: f.id,
|
|
591
|
+
cid: f.entityCid,
|
|
592
|
+
type: "folder",
|
|
593
|
+
relativePath: f.relativePath
|
|
594
|
+
}));
|
|
595
|
+
const resultFiles = createdFiles.map((f) => ({
|
|
596
|
+
id: f.id,
|
|
597
|
+
cid: f.entityCid,
|
|
598
|
+
type: "file",
|
|
599
|
+
relativePath: f.relativePath
|
|
600
|
+
}));
|
|
601
|
+
return {
|
|
602
|
+
success: errors.length === 0,
|
|
603
|
+
collection: {
|
|
604
|
+
id: collectionId,
|
|
605
|
+
cid: collectionCid,
|
|
606
|
+
created: collectionCreated
|
|
607
|
+
},
|
|
608
|
+
folders: resultFolders,
|
|
609
|
+
files: resultFiles,
|
|
610
|
+
errors
|
|
611
|
+
};
|
|
612
|
+
} catch (err) {
|
|
613
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
614
|
+
reportProgress({
|
|
615
|
+
phase: "error",
|
|
616
|
+
error: errorMsg
|
|
617
|
+
});
|
|
618
|
+
return {
|
|
619
|
+
success: false,
|
|
620
|
+
collection: {
|
|
621
|
+
id: target.collectionId ?? "",
|
|
622
|
+
cid: "",
|
|
623
|
+
created: false
|
|
624
|
+
},
|
|
625
|
+
folders: createdFolders.map((f) => ({
|
|
626
|
+
id: f.id,
|
|
627
|
+
cid: f.entityCid,
|
|
628
|
+
type: "folder",
|
|
629
|
+
relativePath: f.relativePath
|
|
630
|
+
})),
|
|
631
|
+
files: createdFiles.map((f) => ({
|
|
632
|
+
id: f.id,
|
|
633
|
+
cid: f.entityCid,
|
|
634
|
+
type: "file",
|
|
635
|
+
relativePath: f.relativePath
|
|
636
|
+
})),
|
|
637
|
+
errors: [...errors, { path: "", error: errorMsg }]
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/operations/upload/scanners.ts
|
|
643
|
+
function getMimeType(filename) {
|
|
644
|
+
const ext = filename.toLowerCase().split(".").pop() || "";
|
|
645
|
+
const mimeTypes = {
|
|
646
|
+
// Images
|
|
647
|
+
jpg: "image/jpeg",
|
|
648
|
+
jpeg: "image/jpeg",
|
|
649
|
+
png: "image/png",
|
|
650
|
+
gif: "image/gif",
|
|
651
|
+
webp: "image/webp",
|
|
652
|
+
svg: "image/svg+xml",
|
|
653
|
+
ico: "image/x-icon",
|
|
654
|
+
bmp: "image/bmp",
|
|
655
|
+
tiff: "image/tiff",
|
|
656
|
+
tif: "image/tiff",
|
|
657
|
+
// Documents
|
|
658
|
+
pdf: "application/pdf",
|
|
659
|
+
doc: "application/msword",
|
|
660
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
661
|
+
xls: "application/vnd.ms-excel",
|
|
662
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
663
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
664
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
665
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
666
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
667
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
668
|
+
// Text
|
|
669
|
+
txt: "text/plain",
|
|
670
|
+
md: "text/markdown",
|
|
671
|
+
csv: "text/csv",
|
|
672
|
+
html: "text/html",
|
|
673
|
+
htm: "text/html",
|
|
674
|
+
css: "text/css",
|
|
675
|
+
xml: "text/xml",
|
|
676
|
+
rtf: "application/rtf",
|
|
677
|
+
// Code
|
|
678
|
+
js: "text/javascript",
|
|
679
|
+
mjs: "text/javascript",
|
|
680
|
+
ts: "text/typescript",
|
|
681
|
+
jsx: "text/javascript",
|
|
682
|
+
tsx: "text/typescript",
|
|
683
|
+
json: "application/json",
|
|
684
|
+
yaml: "text/yaml",
|
|
685
|
+
yml: "text/yaml",
|
|
686
|
+
// Archives
|
|
687
|
+
zip: "application/zip",
|
|
688
|
+
tar: "application/x-tar",
|
|
689
|
+
gz: "application/gzip",
|
|
690
|
+
rar: "application/vnd.rar",
|
|
691
|
+
"7z": "application/x-7z-compressed",
|
|
692
|
+
// Audio
|
|
693
|
+
mp3: "audio/mpeg",
|
|
694
|
+
wav: "audio/wav",
|
|
695
|
+
ogg: "audio/ogg",
|
|
696
|
+
m4a: "audio/mp4",
|
|
697
|
+
flac: "audio/flac",
|
|
698
|
+
// Video
|
|
699
|
+
mp4: "video/mp4",
|
|
700
|
+
webm: "video/webm",
|
|
701
|
+
avi: "video/x-msvideo",
|
|
702
|
+
mov: "video/quicktime",
|
|
703
|
+
mkv: "video/x-matroska",
|
|
704
|
+
// Fonts
|
|
705
|
+
woff: "font/woff",
|
|
706
|
+
woff2: "font/woff2",
|
|
707
|
+
ttf: "font/ttf",
|
|
708
|
+
otf: "font/otf",
|
|
709
|
+
// Other
|
|
710
|
+
wasm: "application/wasm"
|
|
711
|
+
};
|
|
712
|
+
return mimeTypes[ext] || "application/octet-stream";
|
|
713
|
+
}
|
|
714
|
+
async function scanDirectory(directoryPath, options = {}) {
|
|
715
|
+
const fs = await import("fs/promises");
|
|
716
|
+
const path = await import("path");
|
|
717
|
+
const { ignore = ["node_modules", ".git", ".DS_Store"], includeHidden = false } = options;
|
|
718
|
+
const files = [];
|
|
719
|
+
const folders = [];
|
|
720
|
+
const rootName = path.basename(directoryPath);
|
|
721
|
+
async function scanDir(dirPath, relativePath) {
|
|
722
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
723
|
+
for (const entry of entries) {
|
|
724
|
+
const name = entry.name;
|
|
725
|
+
if (!includeHidden && name.startsWith(".")) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (ignore.some((pattern) => name === pattern || name.match(pattern))) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const fullPath = path.join(dirPath, name);
|
|
732
|
+
const entryRelativePath = relativePath ? `${relativePath}/${name}` : name;
|
|
733
|
+
if (entry.isDirectory()) {
|
|
734
|
+
folders.push({
|
|
735
|
+
name,
|
|
736
|
+
relativePath: entryRelativePath
|
|
737
|
+
});
|
|
738
|
+
await scanDir(fullPath, entryRelativePath);
|
|
739
|
+
} else if (entry.isFile()) {
|
|
740
|
+
const stat = await fs.stat(fullPath);
|
|
741
|
+
files.push({
|
|
742
|
+
name,
|
|
743
|
+
relativePath: entryRelativePath,
|
|
744
|
+
size: stat.size,
|
|
745
|
+
mimeType: getMimeType(name),
|
|
746
|
+
getData: async () => {
|
|
747
|
+
const buffer = await fs.readFile(fullPath);
|
|
748
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
await scanDir(directoryPath, "");
|
|
755
|
+
folders.sort((a, b) => a.relativePath.split("/").length - b.relativePath.split("/").length);
|
|
756
|
+
return { files, folders };
|
|
757
|
+
}
|
|
758
|
+
|
|
153
759
|
// src/operations/folders.ts
|
|
154
760
|
var FolderOperations = class {
|
|
155
761
|
constructor(client) {
|
|
@@ -158,15 +764,32 @@ var FolderOperations = class {
|
|
|
158
764
|
/**
|
|
159
765
|
* Upload a local directory to Arke
|
|
160
766
|
*
|
|
161
|
-
*
|
|
162
|
-
* Steps:
|
|
163
|
-
* 1. Scan directory structure
|
|
164
|
-
* 2. Create folder hierarchy (depth-first)
|
|
165
|
-
* 3. Upload files in parallel (with concurrency limit)
|
|
166
|
-
* 4. Create bidirectional relationships (folder contains file)
|
|
767
|
+
* @deprecated Use uploadTree and scanDirectory instead
|
|
167
768
|
*/
|
|
168
|
-
async uploadDirectory(
|
|
169
|
-
|
|
769
|
+
async uploadDirectory(localPath, options) {
|
|
770
|
+
const tree = await scanDirectory(localPath);
|
|
771
|
+
const result = await uploadTree(this.client, tree, {
|
|
772
|
+
target: {
|
|
773
|
+
collectionId: options.collectionId,
|
|
774
|
+
parentId: options.parentFolderId
|
|
775
|
+
},
|
|
776
|
+
concurrency: options.concurrency,
|
|
777
|
+
onProgress: options.onProgress ? (p) => {
|
|
778
|
+
options.onProgress({
|
|
779
|
+
phase: p.phase === "computing-cids" ? "creating-folders" : p.phase === "creating" ? "uploading-files" : p.phase === "backlinking" ? "linking" : p.phase === "complete" ? "complete" : "scanning",
|
|
780
|
+
totalFiles: p.totalEntities,
|
|
781
|
+
completedFiles: p.completedEntities,
|
|
782
|
+
totalFolders: p.totalParents,
|
|
783
|
+
completedFolders: p.completedParents,
|
|
784
|
+
currentFile: p.currentItem
|
|
785
|
+
});
|
|
786
|
+
} : void 0
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
rootFolder: result.folders[0] || null,
|
|
790
|
+
folders: result.folders,
|
|
791
|
+
files: result.files
|
|
792
|
+
};
|
|
170
793
|
}
|
|
171
794
|
};
|
|
172
795
|
|
|
@@ -241,6 +864,8 @@ export {
|
|
|
241
864
|
NotFoundError,
|
|
242
865
|
ValidationError,
|
|
243
866
|
createArkeClient,
|
|
867
|
+
getAuthorizationHeader,
|
|
868
|
+
isApiKey,
|
|
244
869
|
parseApiError
|
|
245
870
|
};
|
|
246
871
|
//# sourceMappingURL=index.js.map
|