2025-11-20 22:07:37 +00:00
|
|
|
|
const { S3Client, PutObjectCommand, DeleteObjectCommand, HeadBucketCommand, CreateBucketCommand, ListObjectsV2Command, PutBucketPolicyCommand } = require('@aws-sdk/client-s3');
|
|
|
|
|
|
const { Upload } = require('@aws-sdk/lib-storage');
|
|
|
|
|
|
const config = require('../config');
|
|
|
|
|
|
const { log } = require('../middleware/logger');
|
|
|
|
|
|
|
|
|
|
|
|
let s3Client = null;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Инициализация S3 клиента для MinIO
|
|
|
|
|
|
*/
|
|
|
|
|
|
function initMinioClient() {
|
|
|
|
|
|
if (!config.minio.enabled) {
|
|
|
|
|
|
log('info', 'MinIO отключен, используется локальное хранилище');
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const endpoint = config.minio.useSSL
|
|
|
|
|
|
? `https://${config.minio.endpoint}:${config.minio.port}`
|
|
|
|
|
|
: `http://${config.minio.endpoint}:${config.minio.port}`;
|
|
|
|
|
|
|
|
|
|
|
|
s3Client = new S3Client({
|
|
|
|
|
|
endpoint: endpoint,
|
|
|
|
|
|
region: config.minio.region || 'us-east-1',
|
|
|
|
|
|
credentials: {
|
|
|
|
|
|
accessKeyId: config.minio.accessKey,
|
|
|
|
|
|
secretAccessKey: config.minio.secretKey
|
|
|
|
|
|
},
|
|
|
|
|
|
forcePathStyle: true, // Важно для MinIO!
|
|
|
|
|
|
tls: config.minio.useSSL
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
log('info', 'S3 клиент для MinIO инициализирован', {
|
|
|
|
|
|
endpoint: endpoint,
|
|
|
|
|
|
bucket: config.minio.bucket,
|
|
|
|
|
|
region: config.minio.region
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Создать bucket если не существует
|
|
|
|
|
|
ensureBucket();
|
|
|
|
|
|
|
|
|
|
|
|
return s3Client;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Ошибка инициализации S3 клиента', { error: error.message });
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Убедиться что bucket существует
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function ensureBucket() {
|
|
|
|
|
|
if (!s3Client) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Проверить существование bucket
|
|
|
|
|
|
try {
|
|
|
|
|
|
await s3Client.send(new HeadBucketCommand({
|
|
|
|
|
|
Bucket: config.minio.bucket
|
|
|
|
|
|
}));
|
|
|
|
|
|
log('info', `Bucket ${config.minio.bucket} существует`);
|
|
|
|
|
|
} catch (headError) {
|
|
|
|
|
|
// Bucket не существует, создаем
|
|
|
|
|
|
if (headError.name === 'NotFound' || headError.$metadata?.httpStatusCode === 404) {
|
|
|
|
|
|
await s3Client.send(new CreateBucketCommand({
|
|
|
|
|
|
Bucket: config.minio.bucket
|
|
|
|
|
|
}));
|
|
|
|
|
|
log('info', `Bucket ${config.minio.bucket} создан`);
|
|
|
|
|
|
|
|
|
|
|
|
// Установить публичную политику для bucket (опционально)
|
|
|
|
|
|
if (config.minio.publicBucket) {
|
|
|
|
|
|
const policy = {
|
|
|
|
|
|
Version: '2012-10-17',
|
|
|
|
|
|
Statement: [{
|
|
|
|
|
|
Effect: 'Allow',
|
|
|
|
|
|
Principal: { AWS: ['*'] },
|
|
|
|
|
|
Action: ['s3:GetObject'],
|
|
|
|
|
|
Resource: [`arn:aws:s3:::${config.minio.bucket}/*`]
|
|
|
|
|
|
}]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await s3Client.send(new PutBucketPolicyCommand({
|
|
|
|
|
|
Bucket: config.minio.bucket,
|
|
|
|
|
|
Policy: JSON.stringify(policy)
|
|
|
|
|
|
}));
|
|
|
|
|
|
log('info', `Bucket ${config.minio.bucket} установлен как публичный`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw headError;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Ошибка проверки/создания bucket', { error: error.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:31:02 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Нормализовать имя файла для MinIO (только ASCII, транслитерация кириллицы)
|
|
|
|
|
|
* @param {string} filename - Оригинальное имя файла
|
|
|
|
|
|
* @returns {string} - Безопасное имя файла
|
|
|
|
|
|
*/
|
|
|
|
|
|
function normalizeFilename(filename) {
|
|
|
|
|
|
if (!filename) return 'file';
|
|
|
|
|
|
|
|
|
|
|
|
// Извлечь расширение
|
|
|
|
|
|
const ext = filename.split('.').pop() || '';
|
|
|
|
|
|
const nameWithoutExt = filename.substring(0, filename.lastIndexOf('.')) || filename;
|
|
|
|
|
|
|
|
|
|
|
|
// Простая транслитерация кириллицы
|
|
|
|
|
|
const translitMap = {
|
|
|
|
|
|
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
|
|
|
|
|
|
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
|
|
|
|
|
|
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
|
|
|
|
|
|
'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch',
|
|
|
|
|
|
'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya',
|
|
|
|
|
|
'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo',
|
|
|
|
|
|
'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'Y', 'К': 'K', 'Л': 'L', 'М': 'M',
|
|
|
|
|
|
'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U',
|
|
|
|
|
|
'Ф': 'F', 'Х': 'H', 'Ц': 'Ts', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sch',
|
|
|
|
|
|
'Ъ': '', 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Транслитерировать имя
|
|
|
|
|
|
let normalized = nameWithoutExt
|
|
|
|
|
|
.split('')
|
|
|
|
|
|
.map(char => translitMap[char] || char)
|
|
|
|
|
|
.join('');
|
|
|
|
|
|
|
|
|
|
|
|
// Убрать все не-ASCII символы (оставить только буквы, цифры, дефис, подчеркивание)
|
|
|
|
|
|
normalized = normalized.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
|
|
|
|
|
|
|
|
|
|
// Если имя пустое после нормализации, использовать 'file'
|
|
|
|
|
|
if (!normalized || normalized.trim() === '') {
|
|
|
|
|
|
normalized = 'file';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Вернуть с расширением (расширение тоже нормализуем)
|
|
|
|
|
|
const safeExt = ext.replace(/[^a-zA-Z0-9]/g, '') || 'bin';
|
|
|
|
|
|
return safeExt ? `${normalized}.${safeExt}` : normalized;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Извлечь расширение файла безопасным способом
|
|
|
|
|
|
* @param {string} filename - Имя файла
|
|
|
|
|
|
* @returns {string} - Расширение файла
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getFileExtension(filename) {
|
|
|
|
|
|
if (!filename) return 'bin';
|
|
|
|
|
|
|
|
|
|
|
|
const parts = filename.split('.');
|
|
|
|
|
|
if (parts.length < 2) return 'bin';
|
|
|
|
|
|
|
|
|
|
|
|
const ext = parts.pop().toLowerCase();
|
|
|
|
|
|
// Оставить только безопасные символы в расширении
|
|
|
|
|
|
return ext.replace(/[^a-z0-9]/g, '') || 'bin';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 22:07:37 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* Загрузить файл в MinIO через S3 SDK
|
|
|
|
|
|
* @param {Buffer} buffer - Буфер файла
|
|
|
|
|
|
* @param {string} filename - Имя файла
|
|
|
|
|
|
* @param {string} contentType - MIME тип
|
|
|
|
|
|
* @param {string} folder - Папка в bucket (например, 'posts', 'avatars')
|
|
|
|
|
|
* @returns {Promise<string>} - URL файла
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function uploadFile(buffer, filename, contentType, folder = 'posts') {
|
|
|
|
|
|
if (!s3Client) {
|
|
|
|
|
|
throw new Error('S3 клиент не инициализирован');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-02 22:31:02 +00:00
|
|
|
|
// Нормализовать имя файла для MinIO (только ASCII)
|
|
|
|
|
|
const safeFilename = normalizeFilename(filename);
|
|
|
|
|
|
const ext = getFileExtension(filename);
|
|
|
|
|
|
|
|
|
|
|
|
// Генерировать уникальное имя файла (используем только timestamp и random, без оригинального имени)
|
2025-11-20 22:07:37 +00:00
|
|
|
|
const timestamp = Date.now();
|
|
|
|
|
|
const random = Math.round(Math.random() * 1E9);
|
|
|
|
|
|
const objectName = `${folder}/${timestamp}-${random}.${ext}`;
|
|
|
|
|
|
|
|
|
|
|
|
// Загрузить файл через S3 SDK
|
|
|
|
|
|
const upload = new Upload({
|
|
|
|
|
|
client: s3Client,
|
|
|
|
|
|
params: {
|
|
|
|
|
|
Bucket: config.minio.bucket,
|
2025-12-02 22:31:02 +00:00
|
|
|
|
Key: objectName, // Используем безопасное имя для Key
|
2025-11-20 22:07:37 +00:00
|
|
|
|
Body: buffer,
|
|
|
|
|
|
ContentType: contentType,
|
|
|
|
|
|
CacheControl: 'public, max-age=31536000', // 1 год
|
|
|
|
|
|
Metadata: {
|
2025-12-02 22:31:02 +00:00
|
|
|
|
// Сохраняем оригинальное имя в метаданных (URL-encoded для безопасности)
|
|
|
|
|
|
originalname: encodeURIComponent(filename),
|
2025-11-20 22:07:37 +00:00
|
|
|
|
uploadedAt: new Date().toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await upload.done();
|
|
|
|
|
|
|
|
|
|
|
|
// Вернуть URL файла
|
|
|
|
|
|
const fileUrl = getFileUrl(objectName);
|
|
|
|
|
|
|
|
|
|
|
|
log('info', 'Файл загружен в MinIO через S3', {
|
|
|
|
|
|
objectName,
|
2025-12-02 22:31:02 +00:00
|
|
|
|
originalName: filename,
|
2025-11-20 22:07:37 +00:00
|
|
|
|
size: buffer.length,
|
|
|
|
|
|
url: fileUrl
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return fileUrl;
|
|
|
|
|
|
} catch (error) {
|
2025-12-02 22:31:02 +00:00
|
|
|
|
log('error', 'Ошибка загрузки файла в MinIO', {
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
filename: filename
|
|
|
|
|
|
});
|
2025-11-20 22:07:37 +00:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Удалить файл из MinIO через S3 SDK
|
|
|
|
|
|
* @param {string} fileUrl - URL файла или путь к объекту
|
|
|
|
|
|
* @returns {Promise<boolean>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function deleteFile(fileUrl) {
|
|
|
|
|
|
if (!s3Client) {
|
|
|
|
|
|
throw new Error('S3 клиент не инициализирован');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Извлечь путь к объекту из URL
|
|
|
|
|
|
const objectName = extractObjectName(fileUrl);
|
|
|
|
|
|
|
|
|
|
|
|
if (!objectName) {
|
|
|
|
|
|
log('warn', 'Не удалось извлечь имя объекта из URL', { fileUrl });
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await s3Client.send(new DeleteObjectCommand({
|
|
|
|
|
|
Bucket: config.minio.bucket,
|
|
|
|
|
|
Key: objectName
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
log('info', 'Файл удален из MinIO через S3', { objectName });
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Ошибка удаления файла из MinIO', { error: error.message });
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Удалить несколько файлов
|
|
|
|
|
|
* @param {string[]} fileUrls - Массив URL файлов
|
|
|
|
|
|
* @returns {Promise<number>} - Количество удаленных файлов
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function deleteFiles(fileUrls) {
|
2025-12-02 22:31:02 +00:00
|
|
|
|
if (!s3Client || !fileUrls || !fileUrls.length) {
|
2025-11-20 22:07:37 +00:00
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let deleted = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (const fileUrl of fileUrls) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const success = await deleteFile(fileUrl);
|
|
|
|
|
|
if (success) deleted++;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Ошибка при удалении файла', { fileUrl, error: error.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return deleted;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Получить временный URL для доступа к файлу (presigned URL)
|
|
|
|
|
|
* @param {string} objectName - Имя объекта
|
|
|
|
|
|
* @param {number} expirySeconds - Время жизни URL в секундах (по умолчанию 7 дней)
|
|
|
|
|
|
* @returns {Promise<string>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function getPresignedUrl(objectName, expirySeconds = 7 * 24 * 60 * 60) {
|
|
|
|
|
|
if (!s3Client) {
|
|
|
|
|
|
throw new Error('S3 клиент не инициализирован');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
|
|
|
|
|
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
|
|
|
|
|
|
|
|
|
|
const command = new GetObjectCommand({
|
|
|
|
|
|
Bucket: config.minio.bucket,
|
|
|
|
|
|
Key: objectName
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const url = await getSignedUrl(s3Client, command, { expiresIn: expirySeconds });
|
|
|
|
|
|
return url;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Ошибка получения presigned URL', { error: error.message });
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Получить публичный URL файла
|
|
|
|
|
|
* @param {string} objectName - Имя объекта
|
|
|
|
|
|
* @returns {string}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getFileUrl(objectName) {
|
|
|
|
|
|
if (config.minio.publicUrl) {
|
|
|
|
|
|
// Использовать кастомный публичный URL (например, через CDN)
|
|
|
|
|
|
return `${config.minio.publicUrl}/${config.minio.bucket}/${objectName}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Использовать прямой URL MinIO
|
|
|
|
|
|
const protocol = config.minio.useSSL ? 'https' : 'http';
|
|
|
|
|
|
const port = config.minio.port === 80 || config.minio.port === 443 ? '' : `:${config.minio.port}`;
|
|
|
|
|
|
return `${protocol}://${config.minio.endpoint}${port}/${config.minio.bucket}/${objectName}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Извлечь имя объекта из URL
|
|
|
|
|
|
* @param {string} fileUrl - URL файла
|
|
|
|
|
|
* @returns {string|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
function extractObjectName(fileUrl) {
|
|
|
|
|
|
if (!fileUrl) return null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Если это уже имя объекта (путь)
|
|
|
|
|
|
if (!fileUrl.startsWith('http')) {
|
|
|
|
|
|
return fileUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Извлечь из URL
|
|
|
|
|
|
const url = new URL(fileUrl);
|
|
|
|
|
|
const pathParts = url.pathname.split('/');
|
|
|
|
|
|
|
|
|
|
|
|
// Убрать bucket из пути
|
|
|
|
|
|
const bucketIndex = pathParts.indexOf(config.minio.bucket);
|
|
|
|
|
|
if (bucketIndex !== -1) {
|
|
|
|
|
|
return pathParts.slice(bucketIndex + 1).join('/');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Попробовать альтернативный формат
|
|
|
|
|
|
return pathParts.slice(1).join('/');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Ошибка парсинга URL', { fileUrl, error: error.message });
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Проверить доступность MinIO через S3 SDK
|
|
|
|
|
|
* @returns {Promise<boolean>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function checkConnection() {
|
|
|
|
|
|
if (!s3Client) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await s3Client.send(new HeadBucketCommand({
|
|
|
|
|
|
Bucket: config.minio.bucket
|
|
|
|
|
|
}));
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'MinIO недоступен', { error: error.message });
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Получить статистику bucket через S3 SDK
|
|
|
|
|
|
* @returns {Promise<object>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function getBucketStats() {
|
|
|
|
|
|
if (!s3Client) {
|
|
|
|
|
|
throw new Error('S3 клиент не инициализирован');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
let totalSize = 0;
|
|
|
|
|
|
let totalFiles = 0;
|
|
|
|
|
|
let continuationToken = undefined;
|
|
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
|
const response = await s3Client.send(new ListObjectsV2Command({
|
|
|
|
|
|
Bucket: config.minio.bucket,
|
|
|
|
|
|
ContinuationToken: continuationToken
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
if (response.Contents) {
|
|
|
|
|
|
for (const obj of response.Contents) {
|
|
|
|
|
|
totalSize += obj.Size || 0;
|
|
|
|
|
|
totalFiles++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
continuationToken = response.NextContinuationToken;
|
|
|
|
|
|
} while (continuationToken);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
totalFiles,
|
|
|
|
|
|
totalSize,
|
|
|
|
|
|
totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2),
|
|
|
|
|
|
totalSizeGB: (totalSize / (1024 * 1024 * 1024)).toFixed(2),
|
|
|
|
|
|
bucket: config.minio.bucket
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Ошибка получения статистики bucket', { error: error.message });
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
initMinioClient,
|
|
|
|
|
|
uploadFile,
|
|
|
|
|
|
deleteFile,
|
|
|
|
|
|
deleteFiles,
|
|
|
|
|
|
getPresignedUrl,
|
|
|
|
|
|
getFileUrl,
|
|
|
|
|
|
checkConnection,
|
|
|
|
|
|
getBucketStats,
|
|
|
|
|
|
isEnabled: () => config.minio.enabled
|
|
|
|
|
|
};
|
|
|
|
|
|
|