97 lines
3.6 KiB
TypeScript
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 };
|
|
}
|