mnemo_cards/mnemo_cards_backend/lib/storage/minio_service.dart
Dmitry 2b65fb00ee
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
stiff
2026-01-24 20:24:51 +03:00

207 lines
6.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:typed_data';
import 'package:injectable/injectable.dart';
import 'package:minio/minio.dart';
import 'package:uuid/uuid.dart';
import 'minio_config.dart';
/// Service for interacting with MinIO object storage
@lazySingleton
class MinioService {
final MinioConfig _config;
late final Minio _client;
final _uuid = const Uuid();
MinioService(this._config) {
_client = Minio(
endPoint: _config.endpoint,
port: _config.port,
accessKey: _config.accessKey,
secretKey: _config.secretKey,
useSSL: _config.useSSL,
region: _config.region,
enableTrace: true,
);
}
/// Ensures all required buckets exist, creates them if they don't
Future<void> ensureBucketsExist() async {
final buckets = [
MinioConfig.cardImagesBucket,
MinioConfig.voiceAudioBucket,
];
print('Config endpoint: ${_config.endpoint}');
print('Config port: ${_config.port}');
print('Config accessKey: ${_config.accessKey}');
print('Config secretKey: ${_config.secretKey}');
print('Config useSSL: ${_config.useSSL}');
print('Config region: ${_config.region}');
for (final bucket in buckets) {
try {
final exists = await _client.bucketExists(bucket);
if (!exists) {
await _client.makeBucket(bucket);
print('✅ Created MinIO bucket: $bucket');
} else {
print('✅ MinIO bucket exists: $bucket');
}
} catch (e) {
print('❌ Error checking/creating bucket $bucket: $e');
rethrow;
}
}
}
/// Uploads a file to MinIO and returns the object ID
///
/// [bucket] - The bucket name
/// [bytes] - File content as bytes
/// [contentType] - MIME type of the file
/// [objectId] - Optional object ID (UUID). If not provided, generates a new one
///
/// Returns the object ID (UUID) that can be stored in the database
Future<String> uploadFile({
required String bucket,
required Uint8List bytes,
required String contentType,
String? objectId,
}) async {
var id = objectId ?? _uuid.v4();
// Add file extension based on content type if not already present
if (!id.contains('.')) {
if (contentType.startsWith('audio/')) {
// Determine extension from content type
String extension = '.mp3'; // default for audio
if (contentType == 'audio/mpeg' || contentType == 'audio/mp3') {
extension = '.mp3';
} else if (contentType == 'audio/wav') {
extension = '.wav';
} else if (contentType == 'audio/ogg') {
extension = '.ogg';
} else if (contentType == 'audio/flac') {
extension = '.flac';
}
id = '$id$extension';
} else if (contentType.startsWith('image/')) {
// Determine extension from content type
String extension = '.png'; // default for images
if (contentType == 'image/jpeg' || contentType == 'image/jpg') {
extension = '.jpg';
} else if (contentType == 'image/png') {
extension = '.png';
} else if (contentType == 'image/webp') {
extension = '.webp';
} else if (contentType == 'image/gif') {
extension = '.gif';
}
id = '$id$extension';
}
}
try {
// Определяем Cache-Control в зависимости от типа файла
// Для изображений: 7 дней, immutable (не изменяются)
// Для аудио: 7 дней
final cacheControl = contentType.startsWith('image/')
? 'public, max-age=604800, immutable' // 7 дней для изображений
: contentType.startsWith('audio/')
? 'public, max-age=604800' // 7 дней для аудио
: 'public, max-age=86400'; // 1 день для других
await _client.putObject(
bucket,
id,
Stream.value(bytes),
size: bytes.length,
metadata: {
'Content-Type': contentType,
// Попытка установить Cache-Control через metadata
// В некоторых SDK это может добавить префикс X-Amz-Meta-
// Но MinIO может обработать это как стандартный заголовок
'Cache-Control': cacheControl,
},
);
print(
'✅ Uploaded file to MinIO: $bucket/$id (${bytes.length} bytes, Cache-Control: $cacheControl)',
);
return id;
} catch (e) {
print('❌ Error uploading file to MinIO: $e');
rethrow;
}
}
/// Generates a presigned URL for getting a file from MinIO
///
/// [bucket] - The bucket name
/// [objectId] - The object ID (UUID)
/// [expirySeconds] - Optional expiry time in seconds (default: 7 days)
///
/// Returns the presigned URL, or null if the object doesn't exist
Future<String?> getPresignedUrl({
required String bucket,
required String objectId,
int? expirySeconds,
}) async {
final expiry = expirySeconds ?? MinioConfig.presignedUrlExpirySeconds;
try {
// Check if object exists
await _client.statObject(bucket, objectId);
// Generate presigned URL
final url = await _client.presignedGetObject(
bucket,
objectId,
expires: expiry,
);
print(
'✅ Generated presigned URL for $bucket/$objectId (expires in ${expiry}s)',
);
return url;
} catch (e, s) {
print(
'❌ Unexpected error generating presigned URL for $bucket/$objectId: $e\n$s',
);
return null;
}
}
/// Deletes a file from MinIO
///
/// [bucket] - The bucket name
/// [objectId] - The object ID (UUID) to delete
Future<void> deleteFile({
required String bucket,
required String objectId,
}) async {
try {
await _client.removeObject(bucket, objectId);
print('✅ Deleted file from MinIO: $bucket/$objectId');
} catch (e) {
print('❌ Error deleting file from MinIO: $e');
rethrow;
}
}
/// Checks if a file exists in MinIO
///
/// [bucket] - The bucket name
/// [objectId] - The object ID (UUID) to check
///
/// Returns true if the file exists, false otherwise
Future<bool> fileExists({
required String bucket,
required String objectId,
}) async {
try {
await _client.statObject(bucket, objectId);
return true;
} catch (e) {
return false;
}
}
}