import { inject, injectable } from "@codemation/core"; import type { BinaryBody, BinaryStorage, BinaryStorageReadResult, BinaryStorageStatResult, BinaryStorageWriteResult, } from "@codemation/core"; import { BinaryBodyNodeReadableFactory } from "../binary/BinaryBodyNodeReadableFactory"; import { BlobSignClient } from "./BlobSignClient"; import { PairingConfigToken } from "../../pairing/PairingConfigToken"; import type { PairingConfig } from "../../pairing/pairing.types"; @injectable() export class PresignedS3BinaryStorage implements BinaryStorage { readonly driverName = "presigned-s3"; constructor( @inject(BlobSignClient) private readonly blobSignClient: BlobSignClient, @inject(PairingConfigToken) private readonly pairingConfig: PairingConfig, ) {} async write(args: { storageKey: string; body: BinaryBody }): Promise { const bytes = await this.toContiguousBytes(args.body); const [signed] = await this.blobSignClient.getPresignedUrls("PUT", [args.storageKey]); if (!signed) { throw new Error(`PresignedS3BinaryStorage: no URL returned for PUT ${args.storageKey}`); } const response = await fetch(signed.url, { method: "PUT", body: bytes as unknown as BodyInit, headers: { "Content-Length": String(bytes.byteLength) }, }); if (!response.ok) { throw new Error(`PresignedS3BinaryStorage PUT failed: ${response.status} for key ${args.storageKey}`); } return { storageKey: args.storageKey, size: bytes.byteLength }; } private async toContiguousBytes(body: BinaryBody): Promise { if (body instanceof Uint8Array) return Buffer.from(body.buffer, body.byteOffset, body.byteLength); if (body instanceof ArrayBuffer) return Buffer.from(body); const readable = new BinaryBodyNodeReadableFactory(body).create(); const chunks: Buffer[] = []; for await (const chunk of readable) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array)); } // eslint-disable-next-line codemation/no-buffer-everything -- a streaming body has no Content-Length for a single PUT; large streams move to presigned multipart (#116) return Buffer.concat(chunks); } async openReadStream(storageKey: string): Promise { const [signed] = await this.blobSignClient.getPresignedUrls("GET", [storageKey]); if (!signed) { return undefined; } const response = await fetch(signed.url, { method: "GET" }); if (response.status === 404) { return undefined; } if (!response.ok) { throw new Error(`PresignedS3BinaryStorage GET failed: ${response.status} for key ${storageKey}`); } if (!response.body) { return undefined; } const contentLength = response.headers.get("content-length"); return { body: response.body as BinaryStorageReadResult["body"], size: contentLength != null ? Number(contentLength) : undefined, }; } async stat(storageKey: string): Promise { const [signed] = await this.blobSignClient.getPresignedUrls("HEAD", [storageKey]); if (!signed) { return { exists: false }; } const response = await fetch(signed.url, { method: "HEAD" }); if (response.status === 404) { return { exists: false }; } if (!response.ok) { throw new Error(`PresignedS3BinaryStorage HEAD failed: ${response.status} for key ${storageKey}`); } const contentLength = response.headers.get("content-length"); return { exists: true, size: contentLength != null ? Number(contentLength) : undefined }; } async delete(storageKey: string): Promise { const [signed] = await this.blobSignClient.getPresignedUrls("DELETE", [storageKey]); if (!signed) { throw new Error(`PresignedS3BinaryStorage: no URL returned for DELETE ${storageKey}`); } const response = await fetch(signed.url, { method: "DELETE" }); if (!response.ok && response.status !== 404) { throw new Error(`PresignedS3BinaryStorage DELETE failed: ${response.status} for key ${storageKey}`); } } async deleteMany(storageKeys: ReadonlyArray): Promise { if (storageKeys.length === 0) return; const signed = await this.blobSignClient.getPresignedUrls("DELETE", storageKeys); await Promise.all( signed.map(async (s) => { const response = await fetch(s.url, { method: "DELETE" }); if (!response.ok && response.status !== 404) { throw new Error(`PresignedS3BinaryStorage DELETE failed: ${response.status} for key ${s.key}`); } }), ); } async listByPrefix(prefix: string): Promise> { return this.blobSignClient.listKeys(prefix); } async checkConnectivity(): Promise { await this.blobSignClient.listKeys(`${this.pairingConfig.workspaceId}/`); } }