Files
gupi-osint-board/server/objectStorage.ts
T
2026-08-17 09:24:53 +02:00

89 lines
3.7 KiB
TypeScript

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<void>
putObject(key: string, body: Buffer, contentType: string): Promise<{ etag?: string }>
getObject(key: string): Promise<ObjectBody | null>
}
export class MemoryObjectStorage implements ObjectStorage {
readonly bucket: string
readonly provider = 'memory' as const
private readonly objects = new Map<string, Buffer>()
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',
})
}