2025-06-04 16:32:50 +02:00
|
|
|
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<string>();
|
|
|
|
|
|
2025-06-04 16:41:59 +02:00
|
|
|
return new Promise(async (resolve, reject) => {
|
|
|
|
|
try {
|
|
|
|
|
const stream = client.listObjectsV2(bucketName, prefix, recursive);
|
|
|
|
|
|
|
|
|
|
stream.on('data', (obj) => {
|
2025-06-04 17:15:03 +02:00
|
|
|
// Handle folder prefixes returned by MinIO
|
|
|
|
|
if (obj && obj.prefix) {
|
|
|
|
|
folders.add(obj.prefix);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-04 17:09:28 +02:00
|
|
|
// Skip objects without a name
|
|
|
|
|
if (!obj || typeof obj.name !== 'string') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-04 16:56:42 +02:00
|
|
|
if (!recursive) {
|
|
|
|
|
if (prefix) {
|
|
|
|
|
// Extract folder structure when inside a folder
|
|
|
|
|
const relativePath = obj.name.substring(prefix.length);
|
2025-06-04 17:09:28 +02:00
|
|
|
if (!relativePath) return; // Skip if no relative path
|
|
|
|
|
|
2025-06-04 16:56:42 +02:00
|
|
|
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,
|
2025-06-04 17:09:28 +02:00
|
|
|
size: obj.size || 0,
|
|
|
|
|
lastModified: obj.lastModified || new Date(),
|
|
|
|
|
etag: obj.etag || '',
|
2025-06-04 16:56:42 +02:00
|
|
|
isFolder: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// At root level
|
|
|
|
|
const firstSlash = obj.name.indexOf('/');
|
|
|
|
|
|
|
|
|
|
if (obj.name.endsWith('/')) {
|
2025-06-04 17:09:28 +02:00
|
|
|
// This is a folder placeholder created by createFolder
|
2025-06-04 16:56:42 +02:00
|
|
|
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,
|
2025-06-04 17:09:28 +02:00
|
|
|
size: obj.size || 0,
|
|
|
|
|
lastModified: obj.lastModified || new Date(),
|
|
|
|
|
etag: obj.etag || '',
|
2025-06-04 16:56:42 +02:00
|
|
|
isFolder: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-04 16:41:59 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2025-06-04 16:56:42 +02:00
|
|
|
// When recursive, include all files
|
2025-06-04 16:41:59 +02:00
|
|
|
if (!obj.name.endsWith('/')) {
|
|
|
|
|
files.push({
|
|
|
|
|
name: obj.name,
|
2025-06-04 17:09:28 +02:00
|
|
|
size: obj.size || 0,
|
|
|
|
|
lastModified: obj.lastModified || new Date(),
|
|
|
|
|
etag: obj.etag || '',
|
2025-06-04 16:41:59 +02:00
|
|
|
isFolder: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-04 16:32:50 +02:00
|
|
|
}
|
2025-06-04 16:41:59 +02:00
|
|
|
});
|
2025-06-04 16:32:50 +02:00
|
|
|
|
2025-06-04 16:41:59 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2025-06-04 16:32:50 +02:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-04 15:27:43 +02:00
|
|
|
// Upload buffer (alias for uploadFile for compatibility)
|
|
|
|
|
export const uploadBuffer = async (buffer: Buffer, filePath: string, contentType: string) => {
|
|
|
|
|
return uploadFile(filePath, buffer, contentType);
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-04 16:32:50 +02:00
|
|
|
// Generate presigned URL for download
|
|
|
|
|
export const getDownloadUrl = async (fileName: string, expiry: number = 60 * 60) => {
|
|
|
|
|
const client = getMinioClient();
|
|
|
|
|
const bucketName = useRuntimeConfig().minio.bucketName;
|
|
|
|
|
|
2025-06-04 16:56:42 +02:00
|
|
|
// Extract just the filename from the full path
|
2025-06-04 18:14:00 +02:00
|
|
|
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];
|
|
|
|
|
}
|
2025-06-04 16:56:42 +02:00
|
|
|
|
|
|
|
|
// Force download with Content-Disposition header
|
|
|
|
|
const responseHeaders = {
|
|
|
|
|
'response-content-disposition': `attachment; filename="${filename}"`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return await client.presignedGetObject(bucketName, fileName, expiry, responseHeaders);
|
2025-06-04 16:32:50 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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) => {
|
2025-06-04 17:09:28 +02:00
|
|
|
if (obj && obj.name) {
|
|
|
|
|
objectsList.push(obj.name);
|
|
|
|
|
}
|
2025-06-04 16:32:50 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
};
|
2025-06-04 18:14:00 +02:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
2025-06-10 13:59:09 +02:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
};
|