137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
/**
|
|||
|
|
* Object storage configuration for S3-compatible backends (Cloudflare R2, MinIO, AWS S3).
|
||
|
|
* Supports both generic OBJECT_STORAGE_* vars and backward-compatible CLOUDFLARE_* fallback.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { S3Client } from '@aws-sdk/client-s3';
|
||
|
|
import { env } from '@api/config/env';
|
||
|
|
|
||
|
|
const BUCKET_DEFAULTS = {
|
||
|
|
VIDEOS: 'videos',
|
||
|
|
DOCUMENTS: 'documents',
|
||
|
|
MEDIA: 'media'
|
||
|
|
} as const;
|
||
|
|
|
||
|
|
/** Whether we're using the generic S3-compatible config (MinIO, etc.) vs Cloudflare R2 fallback */
|
||
|
|
export function useS3CompatibleConfig(): boolean {
|
||
|
|
return Boolean(
|
||
|
|
env.OBJECT_STORAGE_ENDPOINT && env.OBJECT_STORAGE_ACCESS_KEY_ID && env.OBJECT_STORAGE_SECRET_ACCESS_KEY
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function isCloudflareConfigured(): boolean {
|
||
|
|
return Boolean(env.CLOUDFLARE_ACCOUNT_ID && env.CLOUDFLARE_ACCESS_KEY && env.CLOUDFLARE_SECRET_ACCESS_KEY);
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface StorageConfig {
|
||
|
|
endpoint: string;
|
||
|
|
region: string;
|
||
|
|
accessKeyId: string;
|
||
|
|
secretAccessKey: string;
|
||
|
|
forcePathStyle: boolean;
|
||
|
|
bucketVideos: string;
|
||
|
|
bucketDocuments: string;
|
||
|
|
bucketMedia: string;
|
||
|
|
mediaPublicBaseUrl: string | null;
|
||
|
|
presignUploadExpiresSeconds: number;
|
||
|
|
presignDownloadExpiresSeconds: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveStorageConfig(): StorageConfig {
|
||
|
|
if (useS3CompatibleConfig()) {
|
||
|
|
const forcePathStyle =
|
||
|
|
env.OBJECT_STORAGE_FORCE_PATH_STYLE === 'true' || env.OBJECT_STORAGE_FORCE_PATH_STYLE === '1';
|
||
|
|
return {
|
||
|
|
endpoint: env.OBJECT_STORAGE_ENDPOINT!,
|
||
|
|
region: env.OBJECT_STORAGE_REGION || 'us-east-1',
|
||
|
|
accessKeyId: env.OBJECT_STORAGE_ACCESS_KEY_ID!,
|
||
|
|
secretAccessKey: env.OBJECT_STORAGE_SECRET_ACCESS_KEY!,
|
||
|
|
forcePathStyle,
|
||
|
|
bucketVideos: env.OBJECT_STORAGE_BUCKET_VIDEOS || BUCKET_DEFAULTS.VIDEOS,
|
||
|
|
bucketDocuments: env.OBJECT_STORAGE_BUCKET_DOCUMENTS || BUCKET_DEFAULTS.DOCUMENTS,
|
||
|
|
bucketMedia: env.OBJECT_STORAGE_BUCKET_MEDIA || BUCKET_DEFAULTS.MEDIA,
|
||
|
|
mediaPublicBaseUrl: env.OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL || null,
|
||
|
|
presignUploadExpiresSeconds: 60 * 60, // 1 hour
|
||
|
|
presignDownloadExpiresSeconds: 60 * 60 // 1 hour
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isCloudflareConfigured()) {
|
||
|
|
return {
|
||
|
|
endpoint: `https://${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||
|
|
region: 'auto',
|
||
|
|
accessKeyId: env.CLOUDFLARE_ACCESS_KEY!,
|
||
|
|
secretAccessKey: env.CLOUDFLARE_SECRET_ACCESS_KEY!,
|
||
|
|
forcePathStyle: false,
|
||
|
|
bucketVideos: BUCKET_DEFAULTS.VIDEOS,
|
||
|
|
bucketDocuments: BUCKET_DEFAULTS.DOCUMENTS,
|
||
|
|
bucketMedia: BUCKET_DEFAULTS.MEDIA,
|
||
|
|
mediaPublicBaseUrl: env.CLOUDFLARE_IMAGE_BUCKET_DOMAIN || null,
|
||
|
|
presignUploadExpiresSeconds: 60 * 60,
|
||
|
|
presignDownloadExpiresSeconds: 60 * 60
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error(
|
||
|
|
'Object storage not configured. Set either OBJECT_STORAGE_ENDPOINT, OBJECT_STORAGE_ACCESS_KEY_ID, OBJECT_STORAGE_SECRET_ACCESS_KEY (for MinIO/S3) or CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ACCESS_KEY, CLOUDFLARE_SECRET_ACCESS_KEY (for Cloudflare R2).'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
let cachedConfig: StorageConfig | null = null;
|
||
|
|
|
||
|
|
export function getStorageConfig(): StorageConfig {
|
||
|
|
if (!cachedConfig) {
|
||
|
|
cachedConfig = resolveStorageConfig();
|
||
|
|
if (useS3CompatibleConfig()) {
|
||
|
|
console.log('[storage] Using S3-compatible object storage');
|
||
|
|
} else {
|
||
|
|
console.log('[storage] Using Cloudflare R2 (fallback)');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cachedConfig;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** S3 client for API operations (upload, get, delete). Uses internal endpoint. */
|
||
|
|
let s3ClientInstance: S3Client | null = null;
|
||
|
|
|
||
|
|
export function getS3Client(): S3Client {
|
||
|
|
if (!s3ClientInstance) {
|
||
|
|
const config = getStorageConfig();
|
||
|
|
s3ClientInstance = new S3Client({
|
||
|
|
region: config.region,
|
||
|
|
endpoint: config.endpoint,
|
||
|
|
credentials: {
|
||
|
|
accessKeyId: config.accessKeyId,
|
||
|
|
secretAccessKey: config.secretAccessKey
|
||
|
|
},
|
||
|
|
forcePathStyle: config.forcePathStyle
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return s3ClientInstance;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* S3 client for generating presigned URLs. Uses public endpoint when configured
|
||
|
|
* (e.g. when internal endpoint like minio:9000 is not reachable by browser).
|
||
|
|
*/
|
||
|
|
let presignClientInstance: S3Client | null = null;
|
||
|
|
|
||
|
|
export function getPresignS3Client(): S3Client {
|
||
|
|
if (!presignClientInstance) {
|
||
|
|
const config = getStorageConfig();
|
||
|
|
const endpoint = env.OBJECT_STORAGE_PUBLIC_ENDPOINT || config.endpoint;
|
||
|
|
presignClientInstance = new S3Client({
|
||
|
|
region: config.region,
|
||
|
|
endpoint,
|
||
|
|
credentials: {
|
||
|
|
accessKeyId: config.accessKeyId,
|
||
|
|
secretAccessKey: config.secretAccessKey
|
||
|
|
},
|
||
|
|
forcePathStyle: config.forcePathStyle
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return presignClientInstance;
|
||
|
|
}
|
||
|
|
|
||
|
|
export { BUCKET_DEFAULTS };
|