@beam-network/sdk 0.2.8 → 0.2.10
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/client.d.ts +2 -1
- package/dist/client.js +101 -5
- package/dist/models.d.ts +11 -13
- package/dist/provider-signing.d.ts +16 -2
- package/dist/provider-signing.js +43 -4
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, TransferCancelResponse, TransferCreateResponse, TransferPrepareResponse, TransferStatusInfo } from "./models.js";
|
|
1
|
+
import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, SignedUrlFlow, TransferCancelResponse, TransferCreateResponse, TransferPrepareResponse, TransferStatusInfo } from "./models.js";
|
|
2
2
|
export declare const BEAM_DEFAULT_URL = "https://beamcore.b1m.ai";
|
|
3
3
|
export declare class BeamApiError extends Error {
|
|
4
4
|
readonly status: number;
|
|
@@ -21,6 +21,7 @@ export declare class BeamClient {
|
|
|
21
21
|
name?: string;
|
|
22
22
|
testMode?: boolean;
|
|
23
23
|
urlsExpiresAt?: string;
|
|
24
|
+
signedUrlFlow?: SignedUrlFlow;
|
|
24
25
|
}): Promise<TransferPrepareResponse>;
|
|
25
26
|
attachSignedUrls(transferId: string, input: {
|
|
26
27
|
chunkRoutes: SignedChunkRoute[];
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortMultipartUpload, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signListMultipartUpload, signUploadPartCopy } from "./provider-signing.js";
|
|
1
|
+
import { abortMultipartUpload, computeChunkContentMd5, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, isS3CompatibleProvider, listPartsPageMarker, listPartsVerifyMarker, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signListMultipartUpload, signUploadPartCopy } from "./provider-signing.js";
|
|
2
2
|
export const BEAM_DEFAULT_URL = "https://beamcore.b1m.ai";
|
|
3
3
|
export class BeamApiError extends Error {
|
|
4
4
|
status;
|
|
@@ -36,7 +36,8 @@ export class BeamClient {
|
|
|
36
36
|
chunk_hashes: input.chunkHashes,
|
|
37
37
|
callbacks: input.callbacks,
|
|
38
38
|
test_mode: input.testMode || undefined,
|
|
39
|
-
progressive_mode: input.progressiveMode || undefined
|
|
39
|
+
progressive_mode: input.progressiveMode || undefined,
|
|
40
|
+
signed_url_flow: input.signedUrlFlow
|
|
40
41
|
});
|
|
41
42
|
return this.request("POST", "/transfers/create", body);
|
|
42
43
|
}
|
|
@@ -63,7 +64,8 @@ export class BeamClient {
|
|
|
63
64
|
destinations: input.destinations,
|
|
64
65
|
name: input.name,
|
|
65
66
|
test_mode: input.testMode || undefined,
|
|
66
|
-
urls_expires_at: input.urlsExpiresAt
|
|
67
|
+
urls_expires_at: input.urlsExpiresAt,
|
|
68
|
+
signed_url_flow: input.signedUrlFlow
|
|
67
69
|
}));
|
|
68
70
|
}
|
|
69
71
|
attachSignedUrls(transferId, input) {
|
|
@@ -83,11 +85,14 @@ export class BeamClient {
|
|
|
83
85
|
sources: preparedSources,
|
|
84
86
|
destinations: preparedDestinations,
|
|
85
87
|
name: input.name,
|
|
86
|
-
testMode: input.testMode
|
|
88
|
+
testMode: input.testMode,
|
|
89
|
+
signedUrlFlow: input.signedUrlFlow
|
|
87
90
|
});
|
|
88
91
|
if (!prepared.success) {
|
|
89
92
|
return prepared;
|
|
90
93
|
}
|
|
94
|
+
await input.onPrepared?.(prepared);
|
|
95
|
+
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
91
96
|
const destinationsById = new Map();
|
|
92
97
|
preparedDestinations.forEach((preparedDestination, index) => {
|
|
93
98
|
destinationsById.set(preparedDestination.destination_id, input.destinations[index]);
|
|
@@ -96,10 +101,30 @@ export class BeamClient {
|
|
|
96
101
|
preparedSources.forEach((preparedSource, index) => {
|
|
97
102
|
sourcesById.set(preparedSource.source_id, input.sources[index]);
|
|
98
103
|
});
|
|
104
|
+
const signedUrlFlow = prepared.signed_url_flow ?? input.signedUrlFlow ?? "signed_url_v1";
|
|
105
|
+
const checksumCache = new ChunkChecksumCache({
|
|
106
|
+
concurrency: 4,
|
|
107
|
+
expiresIn,
|
|
108
|
+
fetchImpl: this.fetchImpl
|
|
109
|
+
});
|
|
110
|
+
if (signedUrlFlow === "signed_url_v1") {
|
|
111
|
+
for (const chunk of prepared.chunk_plan) {
|
|
112
|
+
const needsChecksum = chunk.destinations.some((target) => {
|
|
113
|
+
const destination = destinationsById.get(target.destination_id);
|
|
114
|
+
return destination ? isS3CompatibleProvider(destination) : false;
|
|
115
|
+
});
|
|
116
|
+
if (!needsChecksum)
|
|
117
|
+
continue;
|
|
118
|
+
const source = sourcesById.get(chunk.source_id);
|
|
119
|
+
if (source)
|
|
120
|
+
checksumCache.preload(source, chunk);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
99
123
|
const routes = [];
|
|
100
124
|
const multipartUploads = new Map();
|
|
101
125
|
try {
|
|
102
126
|
for (const chunk of prepared.chunk_plan) {
|
|
127
|
+
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
103
128
|
for (const target of chunk.destinations) {
|
|
104
129
|
const destination = destinationsById.get(target.destination_id);
|
|
105
130
|
if (!destination) {
|
|
@@ -158,7 +183,9 @@ export class BeamClient {
|
|
|
158
183
|
finalObjectKey,
|
|
159
184
|
partNumber: typeof metadata.part_number === "number"
|
|
160
185
|
? metadata.part_number
|
|
161
|
-
: chunk.source_chunk_index + 1
|
|
186
|
+
: chunk.source_chunk_index + 1,
|
|
187
|
+
signedUrlFlow,
|
|
188
|
+
checksumCache
|
|
162
189
|
}));
|
|
163
190
|
}
|
|
164
191
|
}
|
|
@@ -168,6 +195,7 @@ export class BeamClient {
|
|
|
168
195
|
throw error;
|
|
169
196
|
}
|
|
170
197
|
try {
|
|
198
|
+
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
171
199
|
const attached = await this.attachSignedUrls(prepared.transfer_id, {
|
|
172
200
|
chunkRoutes: routes,
|
|
173
201
|
transferKey: prepared.transfer_key
|
|
@@ -181,6 +209,7 @@ export class BeamClient {
|
|
|
181
209
|
throw error;
|
|
182
210
|
}
|
|
183
211
|
if (input.distribute !== false) {
|
|
212
|
+
await input.throwIfCancelled?.(prepared.transfer_id);
|
|
184
213
|
await this.distributeTransfer(prepared.transfer_id);
|
|
185
214
|
}
|
|
186
215
|
return prepared;
|
|
@@ -202,6 +231,32 @@ export class BeamClient {
|
|
|
202
231
|
if (!input.target.object_key) {
|
|
203
232
|
throw new Error("destination signing target is missing object_key");
|
|
204
233
|
}
|
|
234
|
+
if (input.signedUrlFlow === "signed_url_v1") {
|
|
235
|
+
const contentMd5 = await input.checksumCache.get(input.source, input.chunk);
|
|
236
|
+
const partVerifyUrl = await signListMultipartUpload(input.destination, input.finalObjectKey, input.upload.uploadId, input.expiresIn, { maxParts: 1, partNumberMarker: listPartsVerifyMarker(input.partNumber) });
|
|
237
|
+
const listPageUrl = await signListMultipartUpload(input.destination, input.finalObjectKey, input.upload.uploadId, input.expiresIn, { maxParts: 1000, partNumberMarker: listPartsPageMarker(input.partNumber) });
|
|
238
|
+
return signDestinationRoute({
|
|
239
|
+
chunk: input.chunk,
|
|
240
|
+
target: input.target,
|
|
241
|
+
source: input.source,
|
|
242
|
+
destination: input.destination,
|
|
243
|
+
expiresIn: input.expiresIn,
|
|
244
|
+
uploadId: input.upload.uploadId,
|
|
245
|
+
partNumber: input.partNumber,
|
|
246
|
+
multipartObjectKey: input.finalObjectKey,
|
|
247
|
+
completeUrl: input.upload.completeUrl,
|
|
248
|
+
abortUrl: input.upload.abortUrl,
|
|
249
|
+
listUrl: input.upload.listUrl,
|
|
250
|
+
partVerifyUrl,
|
|
251
|
+
listPageUrl,
|
|
252
|
+
contentMd5,
|
|
253
|
+
transferId: input.transferId,
|
|
254
|
+
multipartGroupId: input.upload.multipartGroupId,
|
|
255
|
+
multipartCreatedAt: input.upload.multipartCreatedAt,
|
|
256
|
+
commitMethod: "upload_part",
|
|
257
|
+
fetchImpl: this.fetchImpl
|
|
258
|
+
});
|
|
259
|
+
}
|
|
205
260
|
const stagingObjectKey = buildStagingObjectKey({
|
|
206
261
|
finalObjectKey: input.finalObjectKey,
|
|
207
262
|
transferId: input.transferId,
|
|
@@ -287,6 +342,47 @@ export class BeamClient {
|
|
|
287
342
|
return (text ? JSON.parse(text) : {});
|
|
288
343
|
}
|
|
289
344
|
}
|
|
345
|
+
class ChunkChecksumCache {
|
|
346
|
+
options;
|
|
347
|
+
queue = [];
|
|
348
|
+
cache = new Map();
|
|
349
|
+
active = 0;
|
|
350
|
+
constructor(options) {
|
|
351
|
+
this.options = options;
|
|
352
|
+
}
|
|
353
|
+
preload(source, chunk) {
|
|
354
|
+
void this.get(source, chunk).catch(() => undefined);
|
|
355
|
+
}
|
|
356
|
+
get(source, chunk) {
|
|
357
|
+
const key = `${chunk.source_id}:${chunk.source_chunk_index}`;
|
|
358
|
+
let existing = this.cache.get(key);
|
|
359
|
+
if (!existing) {
|
|
360
|
+
existing = this.run(() => computeChunkContentMd5({
|
|
361
|
+
source,
|
|
362
|
+
chunk,
|
|
363
|
+
expiresIn: this.options.expiresIn,
|
|
364
|
+
fetchImpl: this.options.fetchImpl
|
|
365
|
+
}));
|
|
366
|
+
this.cache.set(key, existing);
|
|
367
|
+
}
|
|
368
|
+
return existing;
|
|
369
|
+
}
|
|
370
|
+
run(task) {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
const start = () => {
|
|
373
|
+
this.active += 1;
|
|
374
|
+
task().then(resolve, reject).finally(() => {
|
|
375
|
+
this.active -= 1;
|
|
376
|
+
this.queue.shift()?.();
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
if (this.active < Math.max(1, this.options.concurrency))
|
|
380
|
+
start();
|
|
381
|
+
else
|
|
382
|
+
this.queue.push(start);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
290
386
|
export function calculateOptimalChunkSize(totalSize) {
|
|
291
387
|
const mb = 1024 * 1024;
|
|
292
388
|
const gb = 1024 * mb;
|
package/dist/models.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type SignedUrlFlow = "signed_url_v1" | "signed_url_v2";
|
|
1
2
|
export interface BeamClientOptions {
|
|
2
3
|
apiKey?: string;
|
|
3
4
|
baseUrl?: string;
|
|
@@ -44,6 +45,7 @@ export interface TransferCreateRequest {
|
|
|
44
45
|
callbacks?: CallbackConfig[];
|
|
45
46
|
test_mode?: boolean;
|
|
46
47
|
progressive_mode?: boolean;
|
|
48
|
+
signed_url_flow?: SignedUrlFlow;
|
|
47
49
|
}
|
|
48
50
|
export interface RawTransferCreateInput {
|
|
49
51
|
sources: SourceConfig[];
|
|
@@ -59,6 +61,7 @@ export interface RawTransferCreateInput {
|
|
|
59
61
|
*/
|
|
60
62
|
testMode?: boolean;
|
|
61
63
|
progressiveMode?: boolean;
|
|
64
|
+
signedUrlFlow?: SignedUrlFlow;
|
|
62
65
|
}
|
|
63
66
|
export interface TransferCreateResponse {
|
|
64
67
|
success: boolean;
|
|
@@ -104,20 +107,10 @@ export interface DestinationStatusInfo {
|
|
|
104
107
|
}
|
|
105
108
|
export interface TransferStatusInfo {
|
|
106
109
|
transfer_id: string;
|
|
107
|
-
name?: string;
|
|
108
110
|
status: string;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
total_destinations?: number;
|
|
113
|
-
chunks_completed?: number;
|
|
114
|
-
destinations_completed?: number;
|
|
115
|
-
sources?: SourceStatusInfo[];
|
|
116
|
-
destinations?: DestinationStatusInfo[];
|
|
117
|
-
created_at?: string;
|
|
118
|
-
started_at?: string;
|
|
119
|
-
completed_at?: string;
|
|
120
|
-
error_message?: string;
|
|
111
|
+
error_message: string | null;
|
|
112
|
+
started_at: string | null;
|
|
113
|
+
completed_at: string | null;
|
|
121
114
|
}
|
|
122
115
|
export interface S3ProviderConfig {
|
|
123
116
|
provider: "s3";
|
|
@@ -194,6 +187,9 @@ export interface ProviderTransferCreateInput {
|
|
|
194
187
|
* Defaults to true. Set to false to only prepare and attach signed URLs.
|
|
195
188
|
*/
|
|
196
189
|
distribute?: boolean;
|
|
190
|
+
onPrepared?: (prepared: TransferPrepareResponse) => void | Promise<void>;
|
|
191
|
+
throwIfCancelled?: (transferId: string) => void | Promise<void>;
|
|
192
|
+
signedUrlFlow?: SignedUrlFlow;
|
|
197
193
|
}
|
|
198
194
|
export interface PreparedHttpSource {
|
|
199
195
|
source_id: string;
|
|
@@ -238,6 +234,7 @@ export interface SignedChunkRoute {
|
|
|
238
234
|
chunk_size: number;
|
|
239
235
|
expires_at?: string;
|
|
240
236
|
headers?: Record<string, string>;
|
|
237
|
+
dest_headers?: Record<string, string>;
|
|
241
238
|
metadata?: Record<string, unknown>;
|
|
242
239
|
}
|
|
243
240
|
export interface TransferPrepareResponse {
|
|
@@ -252,6 +249,7 @@ export interface TransferPrepareResponse {
|
|
|
252
249
|
logical_chunks?: number;
|
|
253
250
|
total_chunks?: number;
|
|
254
251
|
chunk_plan: ChunkSigningPlanItem[];
|
|
252
|
+
signed_url_flow?: SignedUrlFlow;
|
|
255
253
|
error?: string;
|
|
256
254
|
message?: string;
|
|
257
255
|
}
|
|
@@ -13,7 +13,12 @@ export declare function prepareProviderDestination(destination: ProviderDestinat
|
|
|
13
13
|
export declare function createMultipartUpload(destination: ProviderDestinationConfig, objectKey: string): Promise<string>;
|
|
14
14
|
export declare function signCompleteMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
15
15
|
export declare function signAbortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
|
|
16
|
-
export declare function signListMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number
|
|
16
|
+
export declare function signListMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number, options?: {
|
|
17
|
+
maxParts?: number;
|
|
18
|
+
partNumberMarker?: number;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
export declare function listPartsPageMarker(partNumber: number, pageSize?: number): number | undefined;
|
|
21
|
+
export declare function listPartsVerifyMarker(partNumber: number): number | undefined;
|
|
17
22
|
export declare function signUploadPartCopy(destination: ProviderDestinationConfig, finalObjectKey: string, stagingObjectKey: string, uploadId: string, partNumber: number, expiresIn: number, copySourceRange?: string): Promise<{
|
|
18
23
|
copyUrl: string;
|
|
19
24
|
copySource: string;
|
|
@@ -35,7 +40,10 @@ export declare function signDestinationRoute(input: {
|
|
|
35
40
|
transferId?: string;
|
|
36
41
|
multipartGroupId?: string;
|
|
37
42
|
multipartCreatedAt?: string;
|
|
38
|
-
commitMethod?: "upload_part_copy";
|
|
43
|
+
commitMethod?: "upload_part" | "upload_part_copy";
|
|
44
|
+
contentMd5?: string;
|
|
45
|
+
partVerifyUrl?: string;
|
|
46
|
+
listPageUrl?: string;
|
|
39
47
|
stagingObjectKey?: string;
|
|
40
48
|
copyUrl?: string;
|
|
41
49
|
copySource?: string;
|
|
@@ -43,6 +51,12 @@ export declare function signDestinationRoute(input: {
|
|
|
43
51
|
deleteUrl?: string;
|
|
44
52
|
fetchImpl?: typeof fetch;
|
|
45
53
|
}): Promise<SignedChunkRoute>;
|
|
54
|
+
export declare function computeChunkContentMd5(input: {
|
|
55
|
+
source: ProviderSourceConfig;
|
|
56
|
+
chunk: ChunkSigningPlanItem;
|
|
57
|
+
expiresIn: number;
|
|
58
|
+
fetchImpl?: typeof fetch;
|
|
59
|
+
}): Promise<string>;
|
|
46
60
|
export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
|
|
47
61
|
export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
|
|
48
62
|
export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
|
package/dist/provider-signing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCopyCommand, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
2
3
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
4
|
export function nowIso() {
|
|
@@ -96,13 +97,26 @@ export async function signAbortMultipartUpload(destination, objectKey, uploadId,
|
|
|
96
97
|
}
|
|
97
98
|
throw new Error(`multipart abort is not supported for ${destination.provider}`);
|
|
98
99
|
}
|
|
99
|
-
export async function signListMultipartUpload(destination, objectKey, uploadId, expiresIn) {
|
|
100
|
+
export async function signListMultipartUpload(destination, objectKey, uploadId, expiresIn, options = {}) {
|
|
100
101
|
if (isS3CompatibleProvider(destination)) {
|
|
101
102
|
const client = createS3CompatibleClient(destination);
|
|
102
|
-
return getSignedUrl(client, new ListPartsCommand({
|
|
103
|
+
return getSignedUrl(client, new ListPartsCommand({
|
|
104
|
+
Bucket: destination.bucket,
|
|
105
|
+
Key: objectKey,
|
|
106
|
+
UploadId: uploadId,
|
|
107
|
+
MaxParts: options.maxParts,
|
|
108
|
+
PartNumberMarker: options.partNumberMarker === undefined ? undefined : String(options.partNumberMarker)
|
|
109
|
+
}), { expiresIn });
|
|
103
110
|
}
|
|
104
111
|
throw new Error(`multipart list-parts is not supported for ${destination.provider}`);
|
|
105
112
|
}
|
|
113
|
+
export function listPartsPageMarker(partNumber, pageSize = 1000) {
|
|
114
|
+
const marker = Math.floor((Math.max(1, partNumber) - 1) / pageSize) * pageSize;
|
|
115
|
+
return marker > 0 ? marker : undefined;
|
|
116
|
+
}
|
|
117
|
+
export function listPartsVerifyMarker(partNumber) {
|
|
118
|
+
return partNumber > 1 ? partNumber - 1 : undefined;
|
|
119
|
+
}
|
|
106
120
|
export async function signUploadPartCopy(destination, finalObjectKey, stagingObjectKey, uploadId, partNumber, expiresIn, copySourceRange) {
|
|
107
121
|
if (isS3CompatibleProvider(destination)) {
|
|
108
122
|
const client = createS3CompatibleClient(destination);
|
|
@@ -163,7 +177,8 @@ export async function signDestinationRoute(input) {
|
|
|
163
177
|
uploadId: input.uploadId,
|
|
164
178
|
partNumber: input.partNumber,
|
|
165
179
|
useStagingPut: input.commitMethod === "upload_part_copy",
|
|
166
|
-
fetchImpl: input.fetchImpl
|
|
180
|
+
fetchImpl: input.fetchImpl,
|
|
181
|
+
contentMd5: input.contentMd5
|
|
167
182
|
});
|
|
168
183
|
const sourceRoute = await signSourceRoute({
|
|
169
184
|
source: input.source,
|
|
@@ -182,6 +197,7 @@ export async function signDestinationRoute(input) {
|
|
|
182
197
|
chunk_size: input.chunk.chunk_size,
|
|
183
198
|
expires_at: expiresAtIso(input.expiresIn),
|
|
184
199
|
headers: sourceRoute.headers,
|
|
200
|
+
...(input.contentMd5 ? { dest_headers: { "Content-MD5": input.contentMd5 } } : {}),
|
|
185
201
|
metadata: {
|
|
186
202
|
...(input.target.metadata ?? {}),
|
|
187
203
|
...(input.transferId ? { transfer_id: input.transferId } : {}),
|
|
@@ -195,6 +211,9 @@ export async function signDestinationRoute(input) {
|
|
|
195
211
|
...(input.deleteUrl ? { delete_url: input.deleteUrl } : {}),
|
|
196
212
|
...(input.uploadId ? { upload_id: input.uploadId } : {}),
|
|
197
213
|
...(input.partNumber ? { part_number: input.partNumber } : {}),
|
|
214
|
+
...(input.contentMd5 ? { checksum_algorithm: "content-md5", checksum_value: input.contentMd5 } : {}),
|
|
215
|
+
...(input.partVerifyUrl ? { part_verify_url: input.partVerifyUrl } : {}),
|
|
216
|
+
...(input.listPageUrl ? { list_page_url: input.listPageUrl } : {}),
|
|
198
217
|
...(input.completeUrl ? { complete_url: input.completeUrl } : {}),
|
|
199
218
|
...(input.abortUrl ? { abort_url: input.abortUrl } : {}),
|
|
200
219
|
...(input.listUrl ? { list_url: input.listUrl } : {})
|
|
@@ -238,7 +257,8 @@ async function signDestinationUrl(input) {
|
|
|
238
257
|
Bucket: input.destination.bucket,
|
|
239
258
|
Key: input.uploadPartObjectKey,
|
|
240
259
|
UploadId: input.uploadId,
|
|
241
|
-
PartNumber: input.partNumber
|
|
260
|
+
PartNumber: input.partNumber,
|
|
261
|
+
ContentMD5: input.contentMd5
|
|
242
262
|
}), { expiresIn: input.expiresIn });
|
|
243
263
|
}
|
|
244
264
|
return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey }), { expiresIn: input.expiresIn });
|
|
@@ -248,6 +268,25 @@ async function signDestinationUrl(input) {
|
|
|
248
268
|
}
|
|
249
269
|
return unsupportedProviderConfig(input.destination);
|
|
250
270
|
}
|
|
271
|
+
export async function computeChunkContentMd5(input) {
|
|
272
|
+
const sourceRoute = await signSourceRoute({
|
|
273
|
+
source: input.source,
|
|
274
|
+
fallbackUrl: input.chunk.source_url,
|
|
275
|
+
chunk: input.chunk,
|
|
276
|
+
expiresIn: input.expiresIn,
|
|
277
|
+
fetchImpl: input.fetchImpl
|
|
278
|
+
});
|
|
279
|
+
const fetchImpl = input.fetchImpl ?? globalThis.fetch;
|
|
280
|
+
const response = await fetchImpl(sourceRoute.url, { method: "GET", headers: sourceRoute.headers });
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
throw new Error(`chunk checksum fetch failed with ${response.status}: ${await response.text()}`);
|
|
283
|
+
}
|
|
284
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
285
|
+
if (body.length !== input.chunk.chunk_size) {
|
|
286
|
+
throw new Error(`chunk checksum fetch size mismatch: expected ${input.chunk.chunk_size}, got ${body.length}`);
|
|
287
|
+
}
|
|
288
|
+
return createHash("md5").update(body).digest("base64");
|
|
289
|
+
}
|
|
251
290
|
function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source)) {
|
|
252
291
|
return new S3Client({
|
|
253
292
|
region: s3CompatibleRegion(source),
|