import { Client } from 'minio'; import { config } from './index'; import { logger } from './logger'; /** * MinIO Client Configuration * Used for uploading videos and attachments to S3-compatible storage */ export const minioClient = new Client({ endPoint: new URL(config.s3.endpoint).hostname, port: parseInt(new URL(config.s3.endpoint).port) || (config.s3.useSSL ? 443 : 9000), useSSL: config.s3.useSSL, accessKey: config.s3.accessKey, secretKey: config.s3.secretKey, }); /** * Ensure bucket exists, create if not */ export async function ensureBucketExists(): Promise { try { const exists = await minioClient.bucketExists(config.s3.bucket); if (!exists) { await minioClient.makeBucket(config.s3.bucket, 'us-east-1'); logger.info(`Bucket '${config.s3.bucket}' created successfully`); } } catch (error) { logger.error(`Error ensuring bucket exists: ${error}`); throw error; } } /** * Generate a unique file path for storage */ export function generateFilePath( courseId: number, lessonId: number, fileType: 'video' | 'attachment', originalFilename: string ): string { const timestamp = Date.now(); const uniqueId = Math.random().toString(36).substring(2, 15); const extension = originalFilename.split('.').pop() || ''; const safeFilename = `${timestamp}-${uniqueId}.${extension}`; if (fileType === 'video') { return `courses/${courseId}/lessons/${lessonId}/video/${safeFilename}`; } return `courses/${courseId}/lessons/${lessonId}/attachments/${safeFilename}`; } /** * Upload a file to MinIO */ export async function uploadFile( filePath: string, fileBuffer: Buffer, mimeType: string ): Promise { try { await ensureBucketExists(); await minioClient.putObject( config.s3.bucket, filePath, fileBuffer, fileBuffer.length, { 'Content-Type': mimeType } ); logger.info(`File uploaded successfully: ${filePath}`); return filePath; } catch (error) { logger.error(`Error uploading file: ${error}`); throw error; } } /** * Delete a file from MinIO */ export async function deleteFile(filePath: string): Promise { try { await minioClient.removeObject(config.s3.bucket, filePath); logger.info(`File deleted successfully: ${filePath}`); } catch (error) { logger.error(`Error deleting file: ${error}`); throw error; } } /** * Get a presigned URL for file access */ export async function getPresignedUrl( filePath: string, expirySeconds: number = 3600 ): Promise { try { const url = await minioClient.presignedGetObject( config.s3.bucket, filePath, expirySeconds ); return url; } catch (error) { logger.error(`Error generating presigned URL: ${error}`); throw error; } } /** * List all objects in a path prefix */ export async function listObjects(prefix: string): Promise<{ name: string; size: number; lastModified: Date }[]> { return new Promise((resolve, reject) => { const objects: { name: string; size: number; lastModified: Date }[] = []; const stream = minioClient.listObjectsV2(config.s3.bucket, prefix, true); stream.on('data', (obj) => { if (obj.name) { objects.push({ name: obj.name, size: obj.size || 0, lastModified: obj.lastModified || new Date(), }); } }); stream.on('error', (err) => { logger.error(`Error listing objects: ${err}`); reject(err); }); stream.on('end', () => { resolve(objects); }); }); } /** * List folders in a path prefix * Uses listObjectsV2 with recursive=false to get folder prefixes */ export async function listFolders(prefix: string): Promise { return new Promise((resolve, reject) => { const folders = new Set(); const stream = minioClient.listObjectsV2(config.s3.bucket, prefix, false); stream.on('data', (obj: any) => { if (obj.prefix) { // Direct folder prefix folders.add(obj.prefix); } else if (obj.name) { // Extract folder part from file path const path = obj.name.replace(prefix, ''); const folder = path.split('/')[0]; if (folder && path.includes('/')) { folders.add(prefix + folder + '/'); } } }); stream.on('error', (err) => { logger.error(`Error listing folders: ${err}`); reject(err); }); stream.on('end', () => { resolve(Array.from(folders)); }); }); } /** * Get attachments folder path for a lesson */ export function getAttachmentsFolder(courseId: number, lessonId: number): string { return `courses/${courseId}/lessons/${lessonId}/attachments/`; } /** * Get video folder path for a lesson */ export function getVideoFolder(courseId: number, lessonId: number): string { return `courses/${courseId}/lessons/${lessonId}/video/`; }