89 lines
3.7 KiB
TypeScript
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',
|
||
|
|
})
|
||
|
|
}
|