import { CreateBucketCommand, GetObjectCommand, HeadBucketCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3' import { Readable } from 'node:stream' export type ObjectBody = { stream: Readable; contentLength?: number } export interface ObjectStorage { readonly bucket: string readonly provider: 's3' | 'memory' initialize(): Promise putObject(key: string, body: Buffer, contentType: string): Promise<{ etag?: string }> getObject(key: string): Promise } export class MemoryObjectStorage implements ObjectStorage { readonly bucket: string readonly provider = 'memory' as const private readonly objects = new Map() constructor(bucket = 'osint-test-assets') { this.bucket = bucket } async initialize() { /* Nothing to initialize. */ } async putObject(key: string, body: Buffer) { this.objects.set(key, Buffer.from(body)); return {} } async getObject(key: string) { const body = this.objects.get(key) return body ? { stream: Readable.from(body), contentLength: body.byteLength } : null } } export class S3ObjectStorage implements ObjectStorage { readonly provider = 's3' as const readonly bucket: string private readonly client: S3Client constructor(options: { endpoint: string; region: string; accessKey: string; secretKey: string; bucket: string; forcePathStyle: boolean }) { this.bucket = options.bucket this.client = new S3Client({ endpoint: options.endpoint, region: options.region, forcePathStyle: options.forcePathStyle, credentials: { accessKeyId: options.accessKey, secretAccessKey: options.secretKey }, }) } async initialize() { try { await this.client.send(new HeadBucketCommand({ Bucket: this.bucket })) } catch (error) { const status = (error as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode if (status !== 404) throw error try { await this.client.send(new CreateBucketCommand({ Bucket: this.bucket })) } catch (createError) { const createStatus = (createError as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode if (createStatus !== 409) throw createError } } } async putObject(key: string, body: Buffer, contentType: string) { const result = await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: body, ContentType: contentType })) return { etag: result.ETag?.replaceAll('"', '') } } async getObject(key: string) { try { const result = await this.client.send(new GetObjectCommand({ Bucket: this.bucket, Key: key })) if (!result.Body || typeof (result.Body as NodeJS.ReadableStream).pipe !== 'function') throw new Error(`Object ${key} did not return a Node stream`) return { stream: result.Body as Readable, contentLength: result.ContentLength } } catch (error) { const status = (error as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode if (status === 404) return null throw error } } } export function createObjectStorageFromEnv() { if (process.env.ASSET_STORAGE_DRIVER === 'memory' || process.env.NODE_ENV === 'test') return new MemoryObjectStorage(process.env.S3_BUCKET) const accessKey = process.env.S3_ACCESS_KEY const secretKey = process.env.S3_SECRET_KEY if (!accessKey || !secretKey) throw new Error('S3_ACCESS_KEY and S3_SECRET_KEY are required for MinIO asset storage') return new S3ObjectStorage({ endpoint: process.env.S3_ENDPOINT || 'http://127.0.0.1:9000', region: process.env.S3_REGION || 'us-east-1', accessKey, secretKey, bucket: process.env.S3_BUCKET || 'osint-evidence', forcePathStyle: process.env.S3_FORCE_PATH_STYLE !== 'false', }) }