115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
import type {
|
|
PresignedUploadPartCapability,
|
|
PresignedUploadPartPort,
|
|
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
|
|
import type {
|
|
UploadPartExecutor,
|
|
UploadPartReceipt,
|
|
UploadProviderResult,
|
|
} from "../../../application/ports/browser-transfer/resumable-upload.ts";
|
|
import {
|
|
browserDataFailure,
|
|
browserDataSuccess,
|
|
} from "../../browser-file-storage/result.ts";
|
|
import {
|
|
MEDIA_TYPE,
|
|
isSafeUploadReceiptToken,
|
|
SHA256_HEX,
|
|
} from "./checkpoint-schema.ts";
|
|
|
|
export function createPresignedUploadPartExecutor(
|
|
inputPort: PresignedUploadPartPort,
|
|
now: () => number = Date.now,
|
|
): UploadPartExecutor<PresignedUploadPartCapability> {
|
|
const put = inputPort?.put;
|
|
if (typeof put !== "function" || typeof now !== "function") {
|
|
throw new TypeError("Presigned upload part dependency is invalid.");
|
|
}
|
|
const executor: UploadPartExecutor<PresignedUploadPartCapability> = {
|
|
async uploadPart(
|
|
input: Parameters<
|
|
UploadPartExecutor<PresignedUploadPartCapability>["uploadPart"]
|
|
>[0],
|
|
): Promise<
|
|
UploadProviderResult<UploadPartReceipt>
|
|
> {
|
|
const capability = input.capability;
|
|
let nowEpochMs: number;
|
|
try {
|
|
nowEpochMs = now();
|
|
} catch {
|
|
return browserDataFailure("UNAVAILABLE", "UPLOAD_PART", {
|
|
retryable: true,
|
|
recovery: "RESUME",
|
|
});
|
|
}
|
|
if (
|
|
!capability ||
|
|
capability.method !== "PUT" ||
|
|
capability.binding.kind !== "UPLOAD_PART" ||
|
|
capability.binding.protocol !== input.protocol ||
|
|
capability.binding.sessionId !== input.sessionId ||
|
|
capability.binding.requestBindingSha256 !==
|
|
input.requestBindingSha256 ||
|
|
capability.binding.uploadBindingSha256 !==
|
|
input.uploadBindingSha256 ||
|
|
capability.binding.partNumber !== input.part.partNumber ||
|
|
capability.binding.offset !== input.part.offset ||
|
|
capability.binding.idempotencyKey !== input.idempotencyKey ||
|
|
capability.mediaType !== input.mediaType ||
|
|
!MEDIA_TYPE.test(capability.mediaType) ||
|
|
capability.byteLength !== input.part.byteLength ||
|
|
capability.maxBytes < capability.byteLength ||
|
|
capability.maxBytes !== input.part.byteLength ||
|
|
capability.expectedSha256 !== input.part.checksumSha256 ||
|
|
!SHA256_HEX.test(capability.expectedSha256) ||
|
|
capability.expiresAtEpochMs <= nowEpochMs ||
|
|
!(input.bytes instanceof Uint8Array) ||
|
|
input.bytes.byteLength !== input.part.byteLength ||
|
|
typeof capability.capabilityReceipt !== "string"
|
|
) {
|
|
return browserDataFailure("POLICY_REJECTED", "UPLOAD_PART");
|
|
}
|
|
let uploaded;
|
|
try {
|
|
uploaded = await put.call(inputPort, {
|
|
capability,
|
|
sessionId: input.sessionId,
|
|
requestBindingSha256: input.requestBindingSha256,
|
|
uploadBindingSha256: input.uploadBindingSha256,
|
|
partNumber: input.part.partNumber,
|
|
offset: input.part.offset,
|
|
byteLength: input.part.byteLength,
|
|
checksumSha256: input.part.checksumSha256,
|
|
idempotencyKey: input.idempotencyKey,
|
|
bytes: Uint8Array.from(input.bytes),
|
|
signal: input.signal,
|
|
});
|
|
} catch {
|
|
return browserDataFailure("UNAVAILABLE", "UPLOAD_PART", {
|
|
retryable: true,
|
|
recovery: "RESUME",
|
|
});
|
|
}
|
|
if (!uploaded.ok) return uploaded;
|
|
if (
|
|
uploaded.value.bytesWritten !== input.part.byteLength ||
|
|
uploaded.value.checksumSha256 !== input.part.checksumSha256 ||
|
|
typeof uploaded.value.receiptToken !== "string" ||
|
|
!isSafeUploadReceiptToken(uploaded.value.receiptToken)
|
|
) {
|
|
return browserDataFailure("INTEGRITY_FAILED", "UPLOAD_PART", {
|
|
recovery: "RECONCILE",
|
|
});
|
|
}
|
|
return browserDataSuccess(
|
|
Object.freeze({
|
|
...input.part,
|
|
receiptToken: uploaded.value.receiptToken,
|
|
}),
|
|
);
|
|
},
|
|
};
|
|
return Object.freeze(executor);
|
|
}
|