import { createHash } from "node:crypto"; import { createReadStream } from "node:fs"; import { stat } from "node:fs/promises"; import type { Readable } from "node:stream"; import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; import type { ObjectStorageProvider, PutObjectResult } from "./types"; export interface S3ProviderOptions { bucket: string; region?: string; /** Endpoint custom pra S3-compatible (MinIO, secao 92) — omitido usa * AWS S3 real. */ endpoint?: string; accessKeyId?: string; secretAccessKey?: string; /** MinIO e a maioria dos S3-compatible precisam de path-style * (`endpoint/bucket/key`) em vez do virtual-hosted-style padrão da AWS. */ forcePathStyle?: boolean; } /** * Backend S3-compatible (agente.md secao 92) — preparado pra AWS S3 e * MinIO. **Nunca exercitado nesta sessão**: não existe um servidor S3/MinIO * disponível neste laboratório pra testar contra. A implementação segue a * API padrão do `@aws-sdk/client-s3`; revisar com um MinIO real antes de * confiar em produção. */ export class S3ObjectStorageProvider implements ObjectStorageProvider { private readonly client: S3Client; private readonly bucket: string; constructor(options: S3ProviderOptions) { this.bucket = options.bucket; this.client = new S3Client({ region: options.region ?? "us-east-1", endpoint: options.endpoint, forcePathStyle: options.forcePathStyle, credentials: options.accessKeyId && options.secretAccessKey ? { accessKeyId: options.accessKeyId, secretAccessKey: options.secretAccessKey } : undefined, }); } async putObject(key: string, sourceFilePath: string): Promise { const [{ size }, checksum] = await Promise.all([stat(sourceFilePath), sha256File(sourceFilePath)]); await this.client.send( new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: createReadStream(sourceFilePath), ContentLength: size, }), ); return { sizeBytes: size, checksum }; } async getObjectStream(key: string): Promise { const result = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key })); return result.Body as Readable; } async deleteObject(key: string): Promise { await this.client.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: key })); } } function sha256File(path: string): Promise { return new Promise((resolve, reject) => { const hash = createHash("sha256"); const stream = createReadStream(path); stream.on("data", (chunk) => hash.update(chunk)); stream.on("end", () => resolve(hash.digest("hex"))); stream.on("error", reject); }); }