Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
264
src/lib/minio.ts
264
src/lib/minio.ts
@@ -1,132 +1,132 @@
|
||||
import * as Minio from 'minio'
|
||||
|
||||
// MinIO client singleton (lazy-initialized to avoid build-time errors)
|
||||
const globalForMinio = globalThis as unknown as {
|
||||
minio: Minio.Client | undefined
|
||||
}
|
||||
|
||||
// Internal endpoint for server-to-server communication
|
||||
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'http://localhost:9000'
|
||||
|
||||
// Public endpoint for browser-accessible URLs (pre-signed URLs)
|
||||
// If not set, falls back to internal endpoint
|
||||
export const MINIO_PUBLIC_ENDPOINT = process.env.MINIO_PUBLIC_ENDPOINT || MINIO_ENDPOINT
|
||||
|
||||
function createMinioClient(): Minio.Client {
|
||||
const url = new URL(MINIO_ENDPOINT)
|
||||
|
||||
const accessKey = process.env.MINIO_ACCESS_KEY
|
||||
const secretKey = process.env.MINIO_SECRET_KEY
|
||||
if (process.env.NODE_ENV === 'production' && (!accessKey || !secretKey)) {
|
||||
throw new Error('MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables are required in production')
|
||||
}
|
||||
|
||||
return new Minio.Client({
|
||||
endPoint: url.hostname,
|
||||
port: url.port ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80),
|
||||
useSSL: url.protocol === 'https:',
|
||||
accessKey: accessKey || 'minioadmin',
|
||||
secretKey: secretKey || 'minioadmin',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MinIO client instance (lazy-initialized).
|
||||
* The client is only created on first access, not at module import time.
|
||||
* This prevents build-time errors when env vars are not available.
|
||||
*/
|
||||
export function getMinioClient(): Minio.Client {
|
||||
if (!globalForMinio.minio) {
|
||||
globalForMinio.minio = createMinioClient()
|
||||
}
|
||||
return globalForMinio.minio
|
||||
}
|
||||
|
||||
// Backward-compatible export — lazy getter via Proxy
|
||||
export const minio: Minio.Client = new Proxy({} as Minio.Client, {
|
||||
get(_target, prop, receiver) {
|
||||
return Reflect.get(getMinioClient(), prop, receiver)
|
||||
},
|
||||
})
|
||||
|
||||
// Default bucket name
|
||||
export const BUCKET_NAME = process.env.MINIO_BUCKET || 'mopc-files'
|
||||
|
||||
/**
|
||||
* Replace internal endpoint with public endpoint in a URL
|
||||
*/
|
||||
function replaceEndpoint(url: string): string {
|
||||
if (MINIO_PUBLIC_ENDPOINT === MINIO_ENDPOINT) {
|
||||
return url
|
||||
}
|
||||
|
||||
try {
|
||||
const internalUrl = new URL(MINIO_ENDPOINT)
|
||||
const publicUrl = new URL(MINIO_PUBLIC_ENDPOINT)
|
||||
return url.replace(
|
||||
`${internalUrl.protocol}//${internalUrl.host}`,
|
||||
`${publicUrl.protocol}//${publicUrl.host}`
|
||||
)
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a pre-signed URL for file download or upload
|
||||
* Uses MINIO_PUBLIC_ENDPOINT for browser-accessible URLs
|
||||
*/
|
||||
export async function getPresignedUrl(
|
||||
bucket: string,
|
||||
objectKey: string,
|
||||
method: 'GET' | 'PUT' = 'GET',
|
||||
expirySeconds: number = 900 // 15 minutes default
|
||||
): Promise<string> {
|
||||
let url: string
|
||||
if (method === 'GET') {
|
||||
url = await minio.presignedGetObject(bucket, objectKey, expirySeconds)
|
||||
} else {
|
||||
url = await minio.presignedPutObject(bucket, objectKey, expirySeconds)
|
||||
}
|
||||
|
||||
// Replace internal endpoint with public endpoint for browser access
|
||||
return replaceEndpoint(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a bucket exists, create if not
|
||||
*/
|
||||
export async function ensureBucket(bucket: string): Promise<void> {
|
||||
const exists = await minio.bucketExists(bucket)
|
||||
if (!exists) {
|
||||
await minio.makeBucket(bucket)
|
||||
console.log(`Created MinIO bucket: ${bucket}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object from MinIO
|
||||
*/
|
||||
export async function deleteObject(
|
||||
bucket: string,
|
||||
objectKey: string
|
||||
): Promise<void> {
|
||||
await minio.removeObject(bucket, objectKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique object key for a project file
|
||||
*/
|
||||
export function generateObjectKey(
|
||||
projectId: string,
|
||||
fileName: string
|
||||
): string {
|
||||
const timestamp = Date.now()
|
||||
const sanitizedName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
return `projects/${projectId}/${timestamp}-${sanitizedName}`
|
||||
}
|
||||
|
||||
import * as Minio from 'minio'
|
||||
|
||||
// MinIO client singleton (lazy-initialized to avoid build-time errors)
|
||||
const globalForMinio = globalThis as unknown as {
|
||||
minio: Minio.Client | undefined
|
||||
}
|
||||
|
||||
// Internal endpoint for server-to-server communication
|
||||
const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'http://localhost:9000'
|
||||
|
||||
// Public endpoint for browser-accessible URLs (pre-signed URLs)
|
||||
// If not set, falls back to internal endpoint
|
||||
export const MINIO_PUBLIC_ENDPOINT = process.env.MINIO_PUBLIC_ENDPOINT || MINIO_ENDPOINT
|
||||
|
||||
function createMinioClient(): Minio.Client {
|
||||
const url = new URL(MINIO_ENDPOINT)
|
||||
|
||||
const accessKey = process.env.MINIO_ACCESS_KEY
|
||||
const secretKey = process.env.MINIO_SECRET_KEY
|
||||
if (process.env.NODE_ENV === 'production' && (!accessKey || !secretKey)) {
|
||||
throw new Error('MINIO_ACCESS_KEY and MINIO_SECRET_KEY environment variables are required in production')
|
||||
}
|
||||
|
||||
return new Minio.Client({
|
||||
endPoint: url.hostname,
|
||||
port: url.port ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80),
|
||||
useSSL: url.protocol === 'https:',
|
||||
accessKey: accessKey || 'minioadmin',
|
||||
secretKey: secretKey || 'minioadmin',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MinIO client instance (lazy-initialized).
|
||||
* The client is only created on first access, not at module import time.
|
||||
* This prevents build-time errors when env vars are not available.
|
||||
*/
|
||||
export function getMinioClient(): Minio.Client {
|
||||
if (!globalForMinio.minio) {
|
||||
globalForMinio.minio = createMinioClient()
|
||||
}
|
||||
return globalForMinio.minio
|
||||
}
|
||||
|
||||
// Backward-compatible export — lazy getter via Proxy
|
||||
export const minio: Minio.Client = new Proxy({} as Minio.Client, {
|
||||
get(_target, prop, receiver) {
|
||||
return Reflect.get(getMinioClient(), prop, receiver)
|
||||
},
|
||||
})
|
||||
|
||||
// Default bucket name
|
||||
export const BUCKET_NAME = process.env.MINIO_BUCKET || 'mopc-files'
|
||||
|
||||
/**
|
||||
* Replace internal endpoint with public endpoint in a URL
|
||||
*/
|
||||
function replaceEndpoint(url: string): string {
|
||||
if (MINIO_PUBLIC_ENDPOINT === MINIO_ENDPOINT) {
|
||||
return url
|
||||
}
|
||||
|
||||
try {
|
||||
const internalUrl = new URL(MINIO_ENDPOINT)
|
||||
const publicUrl = new URL(MINIO_PUBLIC_ENDPOINT)
|
||||
return url.replace(
|
||||
`${internalUrl.protocol}//${internalUrl.host}`,
|
||||
`${publicUrl.protocol}//${publicUrl.host}`
|
||||
)
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a pre-signed URL for file download or upload
|
||||
* Uses MINIO_PUBLIC_ENDPOINT for browser-accessible URLs
|
||||
*/
|
||||
export async function getPresignedUrl(
|
||||
bucket: string,
|
||||
objectKey: string,
|
||||
method: 'GET' | 'PUT' = 'GET',
|
||||
expirySeconds: number = 900 // 15 minutes default
|
||||
): Promise<string> {
|
||||
let url: string
|
||||
if (method === 'GET') {
|
||||
url = await minio.presignedGetObject(bucket, objectKey, expirySeconds)
|
||||
} else {
|
||||
url = await minio.presignedPutObject(bucket, objectKey, expirySeconds)
|
||||
}
|
||||
|
||||
// Replace internal endpoint with public endpoint for browser access
|
||||
return replaceEndpoint(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a bucket exists, create if not
|
||||
*/
|
||||
export async function ensureBucket(bucket: string): Promise<void> {
|
||||
const exists = await minio.bucketExists(bucket)
|
||||
if (!exists) {
|
||||
await minio.makeBucket(bucket)
|
||||
console.log(`Created MinIO bucket: ${bucket}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object from MinIO
|
||||
*/
|
||||
export async function deleteObject(
|
||||
bucket: string,
|
||||
objectKey: string
|
||||
): Promise<void> {
|
||||
await minio.removeObject(bucket, objectKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique object key for a project file
|
||||
*/
|
||||
export function generateObjectKey(
|
||||
projectId: string,
|
||||
fileName: string
|
||||
): string {
|
||||
const timestamp = Date.now()
|
||||
const sanitizedName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
return `projects/${projectId}/${timestamp}-${sanitizedName}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user