@arke-institute/sdk 3.6.0 → 3.6.2
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.
|
@@ -43,6 +43,7 @@ __export(operations_exports, {
|
|
|
43
43
|
isCasConflictError: () => isCasConflictError,
|
|
44
44
|
scanFileList: () => scanFileList,
|
|
45
45
|
scanFileSystemEntries: () => scanFileSystemEntries,
|
|
46
|
+
uploadToEntity: () => uploadToEntity,
|
|
46
47
|
uploadTree: () => uploadTree,
|
|
47
48
|
verifyCid: () => verifyCid,
|
|
48
49
|
withCasRetry: () => withCasRetry
|
|
@@ -859,6 +860,185 @@ function buildUploadTree(items) {
|
|
|
859
860
|
return { files, folders };
|
|
860
861
|
}
|
|
861
862
|
|
|
863
|
+
// src/operations/upload/single.ts
|
|
864
|
+
var MAX_CAS_RETRIES = 3;
|
|
865
|
+
async function uploadToEntity(client, entityId, items, options = {}) {
|
|
866
|
+
if (items.length === 0) {
|
|
867
|
+
throw new Error("At least one upload item is required");
|
|
868
|
+
}
|
|
869
|
+
const { onProgress } = options;
|
|
870
|
+
let progressState = {
|
|
871
|
+
phase: "preparing",
|
|
872
|
+
totalBytes: 0,
|
|
873
|
+
uploadedBytes: 0,
|
|
874
|
+
completedFiles: 0,
|
|
875
|
+
totalFiles: items.length
|
|
876
|
+
};
|
|
877
|
+
const reportProgress = (update) => {
|
|
878
|
+
progressState = { ...progressState, ...update };
|
|
879
|
+
onProgress?.(progressState);
|
|
880
|
+
};
|
|
881
|
+
reportProgress({ phase: "preparing" });
|
|
882
|
+
const prepared = await Promise.all(
|
|
883
|
+
items.map((item) => prepareItem(item))
|
|
884
|
+
);
|
|
885
|
+
const keys = prepared.map((p) => p.key);
|
|
886
|
+
const duplicates = keys.filter((key, i) => keys.indexOf(key) !== i);
|
|
887
|
+
if (duplicates.length > 0) {
|
|
888
|
+
const uniqueDupes = [...new Set(duplicates)];
|
|
889
|
+
throw new Error(
|
|
890
|
+
`Duplicate content keys detected: ${uniqueDupes.map((k) => `"${k}"`).join(", ")}. Each file must have a unique key. Provide explicit keys to resolve.`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
const totalBytes = prepared.reduce((sum, p) => sum + p.size, 0);
|
|
894
|
+
reportProgress({ totalBytes });
|
|
895
|
+
const uploadInfos = await Promise.all(
|
|
896
|
+
prepared.map(async (item) => {
|
|
897
|
+
const { data: presigned, error } = await client.api.POST(
|
|
898
|
+
"/entities/{id}/content/upload-url",
|
|
899
|
+
{
|
|
900
|
+
params: { path: { id: entityId } },
|
|
901
|
+
body: {
|
|
902
|
+
cid: item.cid,
|
|
903
|
+
content_type: item.contentType,
|
|
904
|
+
size: item.size
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
);
|
|
908
|
+
if (error || !presigned) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
`Failed to get upload URL for ${item.key}: ${JSON.stringify(error)}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
...item,
|
|
915
|
+
uploadUrl: presigned.upload_url
|
|
916
|
+
};
|
|
917
|
+
})
|
|
918
|
+
);
|
|
919
|
+
reportProgress({ phase: "uploading", uploadedBytes: 0 });
|
|
920
|
+
let uploadedBytes = 0;
|
|
921
|
+
await Promise.all(
|
|
922
|
+
uploadInfos.map(async (item) => {
|
|
923
|
+
const response = await fetch(item.uploadUrl, {
|
|
924
|
+
method: "PUT",
|
|
925
|
+
headers: { "Content-Type": item.contentType },
|
|
926
|
+
body: item.bytes
|
|
927
|
+
});
|
|
928
|
+
if (!response.ok) {
|
|
929
|
+
throw new Error(
|
|
930
|
+
`Upload to R2 failed for ${item.key}: ${response.statusText}`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
uploadedBytes += item.size;
|
|
934
|
+
reportProgress({ uploadedBytes });
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
const { data: tipData, error: tipError } = await client.api.GET(
|
|
938
|
+
"/entities/{id}/tip",
|
|
939
|
+
{ params: { path: { id: entityId } } }
|
|
940
|
+
);
|
|
941
|
+
if (tipError || !tipData) {
|
|
942
|
+
throw new Error(`Failed to get entity tip: ${JSON.stringify(tipError)}`);
|
|
943
|
+
}
|
|
944
|
+
const prevCid = tipData.cid;
|
|
945
|
+
let currentTip = prevCid;
|
|
946
|
+
reportProgress({ phase: "completing", completedFiles: 0 });
|
|
947
|
+
const contents = [];
|
|
948
|
+
let finalCid = currentTip;
|
|
949
|
+
for (let i = 0; i < uploadInfos.length; i++) {
|
|
950
|
+
const item = uploadInfos[i];
|
|
951
|
+
const result = await completeWithRetry(client, entityId, item, currentTip);
|
|
952
|
+
currentTip = result.cid;
|
|
953
|
+
finalCid = result.cid;
|
|
954
|
+
contents.push({
|
|
955
|
+
key: item.key,
|
|
956
|
+
cid: result.contentCid,
|
|
957
|
+
size: item.size,
|
|
958
|
+
contentType: item.contentType,
|
|
959
|
+
filename: item.filename
|
|
960
|
+
});
|
|
961
|
+
reportProgress({ completedFiles: i + 1 });
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
id: entityId,
|
|
965
|
+
cid: finalCid,
|
|
966
|
+
prevCid,
|
|
967
|
+
contents
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
async function prepareItem(item) {
|
|
971
|
+
const { data } = item;
|
|
972
|
+
let bytes;
|
|
973
|
+
if (data instanceof Blob) {
|
|
974
|
+
bytes = await data.arrayBuffer();
|
|
975
|
+
} else if (data instanceof ArrayBuffer) {
|
|
976
|
+
bytes = data;
|
|
977
|
+
} else {
|
|
978
|
+
const buffer = new ArrayBuffer(data.byteLength);
|
|
979
|
+
new Uint8Array(buffer).set(data);
|
|
980
|
+
bytes = buffer;
|
|
981
|
+
}
|
|
982
|
+
const size = bytes.byteLength;
|
|
983
|
+
let filename = item.filename;
|
|
984
|
+
let contentType = item.contentType;
|
|
985
|
+
if (data instanceof File) {
|
|
986
|
+
filename = filename ?? data.name;
|
|
987
|
+
contentType = contentType ?? (data.type || getMimeType(data.name));
|
|
988
|
+
}
|
|
989
|
+
contentType = contentType ?? "application/octet-stream";
|
|
990
|
+
if (contentType === "application/octet-stream" && filename) {
|
|
991
|
+
contentType = getMimeType(filename);
|
|
992
|
+
}
|
|
993
|
+
const cid = await computeCid(new Uint8Array(bytes));
|
|
994
|
+
let key = item.key;
|
|
995
|
+
if (!key && filename) {
|
|
996
|
+
const lastDot = filename.lastIndexOf(".");
|
|
997
|
+
key = lastDot > 0 ? filename.substring(0, lastDot) : filename;
|
|
998
|
+
}
|
|
999
|
+
if (!key) {
|
|
1000
|
+
key = `file_${cid.slice(-8)}`;
|
|
1001
|
+
}
|
|
1002
|
+
return { key, bytes, size, contentType, filename, cid };
|
|
1003
|
+
}
|
|
1004
|
+
async function completeWithRetry(client, entityId, item, expectTip) {
|
|
1005
|
+
let tip = expectTip;
|
|
1006
|
+
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
1007
|
+
const { data, error, response } = await client.api.POST(
|
|
1008
|
+
"/entities/{id}/content/complete",
|
|
1009
|
+
{
|
|
1010
|
+
params: { path: { id: entityId } },
|
|
1011
|
+
body: {
|
|
1012
|
+
key: item.key,
|
|
1013
|
+
cid: item.cid,
|
|
1014
|
+
size: item.size,
|
|
1015
|
+
content_type: item.contentType,
|
|
1016
|
+
filename: item.filename,
|
|
1017
|
+
expect_tip: tip
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
);
|
|
1021
|
+
if (data) {
|
|
1022
|
+
return { cid: data.cid, contentCid: data.content.cid };
|
|
1023
|
+
}
|
|
1024
|
+
if (response?.status === 409) {
|
|
1025
|
+
const { data: freshTip, error: tipError } = await client.api.GET(
|
|
1026
|
+
"/entities/{id}/tip",
|
|
1027
|
+
{ params: { path: { id: entityId } } }
|
|
1028
|
+
);
|
|
1029
|
+
if (tipError || !freshTip) {
|
|
1030
|
+
throw new Error(`Failed to refresh tip: ${JSON.stringify(tipError)}`);
|
|
1031
|
+
}
|
|
1032
|
+
tip = freshTip.cid;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
`Failed to complete upload for ${item.key}: ${JSON.stringify(error)}`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
throw new Error(`Max CAS retries exceeded for ${item.key}`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
862
1042
|
// src/operations/folders.ts
|
|
863
1043
|
var FolderOperations = class {
|
|
864
1044
|
constructor(client) {
|
|
@@ -1056,6 +1236,7 @@ async function withCasRetry(callbacks, options) {
|
|
|
1056
1236
|
isCasConflictError,
|
|
1057
1237
|
scanFileList,
|
|
1058
1238
|
scanFileSystemEntries,
|
|
1239
|
+
uploadToEntity,
|
|
1059
1240
|
uploadTree,
|
|
1060
1241
|
verifyCid,
|
|
1061
1242
|
withCasRetry
|