import { Client } from 'minio'; import type { BucketItem } from 'minio'; // Initialize MinIO client export const getMinioClient = () => { const config = useRuntimeConfig().minio; return new Client({ endPoint: config.endPoint, port: config.port, useSSL: config.useSSL, accessKey: config.accessKey, secretKey: config.secretKey, }); }; // File listing with metadata export const listFiles = async (prefix: string = '', recursive: boolean = false) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; const files: any[] = []; const folders = new Set(); return new Promise(async (resolve, reject) => { try { const stream = client.listObjectsV2(bucketName, prefix, recursive); stream.on('data', (obj) => { // Handle folder prefixes returned by MinIO if (obj && obj.prefix) { folders.add(obj.prefix); return; } // Skip objects without a name if (!obj || typeof obj.name !== 'string') { return; } if (!recursive) { if (prefix) { // Extract folder structure when inside a folder const relativePath = obj.name.substring(prefix.length); if (!relativePath) return; // Skip if no relative path const firstSlash = relativePath.indexOf('/'); if (firstSlash > -1) { // This is a folder const folderName = relativePath.substring(0, firstSlash); folders.add(prefix + folderName + '/'); } else if (relativePath && !obj.name.endsWith('/')) { // This is a file in the current folder files.push({ name: obj.name, size: obj.size || 0, lastModified: obj.lastModified || new Date(), etag: obj.etag || '', isFolder: false, }); } } else { // At root level const firstSlash = obj.name.indexOf('/'); if (obj.name.endsWith('/')) { // This is a folder placeholder created by createFolder folders.add(obj.name); } else if (firstSlash > -1) { // This is inside a folder, extract the folder const folderName = obj.name.substring(0, firstSlash); folders.add(folderName + '/'); } else { // This is a file at root files.push({ name: obj.name, size: obj.size || 0, lastModified: obj.lastModified || new Date(), etag: obj.etag || '', isFolder: false, }); } } } else { // When recursive, include all files if (!obj.name.endsWith('/')) { files.push({ name: obj.name, size: obj.size || 0, lastModified: obj.lastModified || new Date(), etag: obj.etag || '', isFolder: false, }); } } }); stream.on('error', (error) => { console.error('Stream error:', error); reject(error); }); stream.on('end', () => { // Add folders to the result const folderItems = Array.from(folders).map(folder => ({ name: folder, size: 0, lastModified: new Date(), etag: '', isFolder: true, })); resolve([...folderItems, ...files]); }); } catch (error) { console.error('Error in listFiles:', error); reject(error); } }); }; // Upload file export const uploadFile = async (filePath: string, fileBuffer: Buffer, contentType: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; return await client.putObject(bucketName, filePath, fileBuffer, fileBuffer.length, { 'Content-Type': contentType, }); }; // Upload buffer (alias for uploadFile for compatibility) export const uploadBuffer = async (buffer: Buffer, filePath: string, contentType: string) => { return uploadFile(filePath, buffer, contentType); }; // Generate presigned URL for download export const getDownloadUrl = async (fileName: string, expiry: number = 60 * 60) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; // Extract just the filename from the full path let filename = fileName.split('/').pop() || fileName; // Remove timestamp prefix if present (e.g., "1234567890-filename.pdf" -> "filename.pdf") const timestampMatch = filename.match(/^\d{10,}-(.+)$/); if (timestampMatch) { filename = timestampMatch[1]; } // Force download with Content-Disposition header const responseHeaders = { 'response-content-disposition': `attachment; filename="${filename}"`, }; return await client.presignedGetObject(bucketName, fileName, expiry, responseHeaders); }; // Delete file export const deleteFile = async (fileName: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; return await client.removeObject(bucketName, fileName); }; // Delete folder (recursively delete all contents) export const deleteFolder = async (folderPath: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; // List all objects in the folder const objectsList: string[] = []; return new Promise((resolve, reject) => { const stream = client.listObjectsV2(bucketName, folderPath, true); stream.on('data', (obj) => { if (obj && obj.name) { objectsList.push(obj.name); } }); stream.on('error', reject); stream.on('end', async () => { try { // Delete all objects if (objectsList.length > 0) { await client.removeObjects(bucketName, objectsList); } resolve(true); } catch (error) { reject(error); } }); }); }; // Get file stats export const getFileStats = async (fileName: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; return await client.statObject(bucketName, fileName); }; // Create folder (MinIO doesn't have explicit folders, so we create a placeholder) export const createFolder = async (folderPath: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; // Ensure folder path ends with / const normalizedPath = folderPath.endsWith('/') ? folderPath : folderPath + '/'; // Create an empty object to represent the folder return await client.putObject(bucketName, normalizedPath, Buffer.from(''), 0); }; // Get presigned URL for file preview export const getPreviewUrl = async (fileName: string, contentType: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; // For images and PDFs, generate a presigned URL with appropriate response headers const responseHeaders = { 'response-content-type': contentType, 'response-content-disposition': 'inline', }; return await client.presignedGetObject(bucketName, fileName, 60 * 60, responseHeaders); }; // Rename file (copy and delete) export const renameFile = async (oldPath: string, newPath: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; try { // Copy the object to the new name await client.copyObject( bucketName, newPath, `/${bucketName}/${oldPath}` ); // Delete the old object await client.removeObject(bucketName, oldPath); return true; } catch (error) { console.error('Error renaming file:', error); throw error; } }; // Rename folder (copy all contents and delete) export const renameFolder = async (oldPath: string, newPath: string) => { const client = getMinioClient(); const bucketName = useRuntimeConfig().minio.bucketName; // Ensure paths end with / const oldPrefix = oldPath.endsWith('/') ? oldPath : oldPath + '/'; const newPrefix = newPath.endsWith('/') ? newPath : newPath + '/'; // List all objects in the folder const objectsList: string[] = []; return new Promise((resolve, reject) => { const stream = client.listObjectsV2(bucketName, oldPrefix, true); stream.on('data', (obj) => { if (obj && obj.name) { objectsList.push(obj.name); } }); stream.on('error', reject); stream.on('end', async () => { try { // Copy all objects to new location for (const objectName of objectsList) { const newObjectName = objectName.replace(oldPrefix, newPrefix); await client.copyObject( bucketName, newObjectName, `/${bucketName}/${objectName}` ); } // Delete all old objects if (objectsList.length > 0) { await client.removeObjects(bucketName, objectsList); } resolve(true); } catch (error) { reject(error); } }); }); }; // Create bucket if it doesn't exist export const createBucketIfNotExists = async (bucketName?: string) => { const client = getMinioClient(); const bucket = bucketName || useRuntimeConfig().minio.bucketName; try { const exists = await client.bucketExists(bucket); if (!exists) { await client.makeBucket(bucket); console.log(`Bucket '${bucket}' created successfully`); } return true; } catch (error) { console.error('Error creating bucket:', error); throw error; } };