voxblog/apps/api/src/storage/s3.ts

97 lines
3.6 KiB
TypeScript

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://<VPS_IP>: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<string> {
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<void>((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 };
}