import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; export function getS3Client() { const endpoint = process.env.S3_ENDPOINT; // e.g. http://:9000 const region = process.env.S3_REGION || 'us-east-1'; const accessKeyId = process.env.S3_ACCESS_KEY || ''; const secretAccessKey = process.env.S3_SECRET_KEY || ''; if (!endpoint || !accessKeyId || !secretAccessKey) { throw new Error('Missing S3 config: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY'); } return new S3Client({ region, endpoint, forcePathStyle: true, credentials: { accessKeyId, secretAccessKey }, }); } export async function getPresignedUrl(params: { bucket: string; key: string; expiresInSeconds?: number }): Promise { const s3 = getS3Client(); const cmd = new GetObjectCommand({ Bucket: params.bucket, Key: params.key }); const expiresIn = typeof params.expiresInSeconds === 'number' ? params.expiresInSeconds : 60 * 60 * 24 * 7; // 7 days return await getSignedUrl(s3, cmd, { expiresIn }); } export function getPublicUrlForKey(key: string): string | null { const base = process.env.PUBLIC_MEDIA_BASE_URL; if (!base) return null; return `${base.replace(/\/$/, '')}/${key}`; } export async function listObjects(params: { bucket: string; prefix?: string; maxKeys?: number }) { const s3 = getS3Client(); const cmd = new ListObjectsV2Command({ Bucket: params.bucket, Prefix: params.prefix, MaxKeys: params.maxKeys || 100, }); const res = await s3.send(cmd); const items = (res.Contents || []).map(obj => ({ key: obj.Key || '', size: obj.Size || 0, lastModified: obj.LastModified?.toISOString() || null, })).filter(i => i.key); return { items, isTruncated: !!res.IsTruncated, nextContinuationToken: res.NextContinuationToken }; } export async function deleteObject(params: { bucket: string; key: string }) { const s3 = getS3Client(); await s3.send(new DeleteObjectCommand({ Bucket: params.bucket, Key: params.key })); return { success: true }; } export async function uploadBuffer(params: { bucket: string; key: string; body: Buffer; contentType?: string; }) { const s3 = getS3Client(); console.log('[S3] Upload start', { bucket: params.bucket, key: params.key, bytes: params.body?.length ?? 0, contentType: params.contentType || 'application/octet-stream', }); const cmd = new PutObjectCommand({ Bucket: params.bucket, Key: params.key, Body: params.body, ContentType: params.contentType || 'application/octet-stream', }); await s3.send(cmd); console.log('[S3] Upload done', { bucket: params.bucket, key: params.key }); return { bucket: params.bucket, key: params.key }; } export async function downloadObject(params: { bucket: string; key: string }): Promise<{ buffer: Buffer; contentType: string }> { const s3 = getS3Client(); console.log('[S3] Download start', { bucket: params.bucket, key: params.key }); const cmd = new GetObjectCommand({ Bucket: params.bucket, Key: params.key }); const res = await s3.send(cmd); const contentType = res.ContentType || 'application/octet-stream'; const body = res.Body as unknown as NodeJS.ReadableStream; const chunks: Buffer[] = []; await new Promise((resolve, reject) => { body.on('data', (c: Buffer) => chunks.push(c)); body.on('end', resolve); body.on('error', reject); }); const buffer = Buffer.concat(chunks); console.log('[S3] Download done', { bucket: params.bucket, key: params.key, bytes: buffer.length, contentType }); return { buffer, contentType }; }