deckhearth/lib/object-storage.js

107 lines
2.7 KiB
JavaScript
Raw Permalink Normal View History

import {
DeleteObjectCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
/**
* MinIO / S3-compatible object storage (CT 102).
*
* Env:
* S3_ENDPOINT e.g. http://192.168.68.102:9000
* S3_ACCESS_KEY_ID app-scoped MinIO user
* S3_SECRET_ACCESS_KEY
* S3_BUCKET e.g. deckhearth
* S3_PUBLIC_BASE_URL e.g. https://cdn.stillwell.cloud/deckhearth
* S3_REGION optional, default us-east-1
*/
let client = null;
function requireEnv(name) {
const value = process.env[name];
if (!value?.trim()) {
throw new Error(`${name} is not configured`);
}
return value.trim();
}
function getClient() {
if (!client) {
client = new S3Client({
endpoint: requireEnv('S3_ENDPOINT'),
region: process.env.S3_REGION || 'us-east-1',
credentials: {
accessKeyId: requireEnv('S3_ACCESS_KEY_ID'),
secretAccessKey: requireEnv('S3_SECRET_ACCESS_KEY'),
},
forcePathStyle: true,
});
}
return client;
}
function getBucket() {
return requireEnv('S3_BUCKET');
}
function publicUrlForKey(key) {
const base = requireEnv('S3_PUBLIC_BASE_URL').replace(/\/$/, '');
return `${base}/${key.replace(/^\//, '')}`;
}
/** Extract object key from a stored public URL or pass through raw keys. */
export function objectKeyFromReference(reference) {
if (!reference || typeof reference !== 'string') {
throw new Error('Invalid object reference');
}
if (!reference.startsWith('http')) {
return reference.replace(/^\//, '');
}
const base = process.env.S3_PUBLIC_BASE_URL?.replace(/\/$/, '');
if (base && reference.startsWith(`${base}/`)) {
return reference.slice(base.length + 1);
}
try {
const url = new URL(reference);
const parts = url.pathname.split('/').filter(Boolean);
const bucket = getBucket();
if (parts[0] === bucket) {
return parts.slice(1).join('/');
}
return parts.join('/');
} catch {
throw new Error('Could not parse object URL for deletion');
}
}
/**
* Drop-in replacement for `@vercel/blob` put (access ignored bucket policy controls visibility).
* @returns {Promise<{ url: string }>}
*/
export async function put(key, body, { contentType } = {}) {
const objectKey = key.replace(/^\//, '');
await getClient().send(
new PutObjectCommand({
Bucket: getBucket(),
Key: objectKey,
Body: body,
ContentType: contentType,
})
);
return { url: publicUrlForKey(objectKey) };
}
/** Delete by object key or public URL (legacy blob URLs in DB). */
export async function del(urlOrKey) {
const objectKey = objectKeyFromReference(urlOrKey);
await getClient().send(
new DeleteObjectCommand({
Bucket: getBucket(),
Key: objectKey,
})
);
}