Add MinIO file browser with upload, preview, and management features
- Implement file browser UI with upload/download capabilities - Add API endpoints for file operations (list, upload, delete, preview) - Create FileUploader and FilePreviewModal components - Configure MinIO integration with environment variables - Add documentation for MinIO file browser setup
This commit is contained in:
167
server/utils/minio.ts
Normal file
167
server/utils/minio.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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>();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = client.listObjectsV2(bucketName, prefix, recursive);
|
||||
|
||||
stream.on('data', (obj) => {
|
||||
if (!recursive && prefix) {
|
||||
// Extract folder structure when not recursive
|
||||
const relativePath = obj.name.substring(prefix.length);
|
||||
const firstSlash = relativePath.indexOf('/');
|
||||
|
||||
if (firstSlash > -1) {
|
||||
// This is a folder
|
||||
const folderName = relativePath.substring(0, firstSlash);
|
||||
folders.add(prefix + folderName + '/');
|
||||
} else if (relativePath) {
|
||||
// This is a file in the current folder
|
||||
files.push({
|
||||
name: obj.name,
|
||||
size: obj.size,
|
||||
lastModified: obj.lastModified,
|
||||
etag: obj.etag,
|
||||
isFolder: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// When recursive or at root, include all files
|
||||
if (!obj.name.endsWith('/')) {
|
||||
files.push({
|
||||
name: obj.name,
|
||||
size: obj.size,
|
||||
lastModified: obj.lastModified,
|
||||
etag: obj.etag,
|
||||
isFolder: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', reject);
|
||||
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]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 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,
|
||||
});
|
||||
};
|
||||
|
||||
// Generate presigned URL for download
|
||||
export const getDownloadUrl = async (fileName: string, expiry: number = 60 * 60) => {
|
||||
const client = getMinioClient();
|
||||
const bucketName = useRuntimeConfig().minio.bucketName;
|
||||
|
||||
return await client.presignedGetObject(bucketName, fileName, expiry);
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
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);
|
||||
};
|
||||
Reference in New Issue
Block a user