mnemo_cards/mnemo_cards_backend/lib/storage/minio_service.dart

208 lines
6.4 KiB
Dart
Raw Permalink Normal View History

2025-12-19 00:56:58 +00:00
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,
2025-12-19 01:45:56 +00:00
enableTrace: true,
2025-12-19 00:56:58 +00:00
);
}
/// Ensures all required buckets exist, creates them if they don't
Future<void> ensureBucketsExist() async {
final buckets = [
MinioConfig.cardImagesBucket,
MinioConfig.voiceAudioBucket,
];
2025-12-19 01:45:56 +00:00
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}');
2025-12-19 00:56:58 +00:00
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 {
2026-01-24 17:24:51 +00:00
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';
}
}
2025-12-19 00:56:58 +00:00
try {
2026-01-24 15:18:58 +00:00
// Определяем Cache-Control в зависимости от типа файла
// Для изображений: 7 дней, immutable (не изменяются)
// Для аудио: 7 дней
final cacheControl = contentType.startsWith('image/')
? 'public, max-age=604800, immutable' // 7 дней для изображений
: contentType.startsWith('audio/')
2026-01-24 16:07:29 +00:00
? 'public, max-age=604800' // 7 дней для аудио
: 'public, max-age=86400'; // 1 день для других
2026-01-24 15:18:58 +00:00
2025-12-19 00:56:58 +00:00
await _client.putObject(
bucket,
id,
Stream.value(bytes),
size: bytes.length,
2026-01-24 15:18:58 +00:00
metadata: {
'Content-Type': contentType,
// Попытка установить Cache-Control через metadata
// В некоторых SDK это может добавить префикс X-Amz-Meta-
// Но MinIO может обработать это как стандартный заголовок
'Cache-Control': cacheControl,
},
2025-12-19 00:56:58 +00:00
);
2026-01-24 15:18:58 +00:00
print(
'✅ Uploaded file to MinIO: $bucket/$id (${bytes.length} bytes, Cache-Control: $cacheControl)',
);
2025-12-19 00:56:58 +00:00
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)
2025-12-20 11:25:32 +00:00
/// [expirySeconds] - Optional expiry time in seconds (default: 7 days)
2025-12-19 00:56:58 +00:00
///
/// Returns the presigned URL, or null if the object doesn't exist
Future<String?> getPresignedUrl({
required String bucket,
required String objectId,
int? expirySeconds,
}) async {
2025-12-20 11:25:32 +00:00
final expiry = expirySeconds ?? MinioConfig.presignedUrlExpirySeconds;
2025-12-19 00:56:58 +00:00
try {
// Check if object exists
await _client.statObject(bucket, objectId);
// Generate presigned URL
final url = await _client.presignedGetObject(
bucket,
objectId,
2025-12-20 11:25:32 +00:00
expires: expiry,
2025-12-19 00:56:58 +00:00
);
2025-12-20 18:26:15 +00:00
print(
'✅ Generated presigned URL for $bucket/$objectId (expires in ${expiry}s)',
);
2025-12-19 00:56:58 +00:00
return url;
2025-12-20 11:25:32 +00:00
} catch (e, s) {
2025-12-19 00:56:58 +00:00
print(
2025-12-20 11:25:32 +00:00
'❌ Unexpected error generating presigned URL for $bucket/$objectId: $e\n$s',
2025-12-19 00:56:58 +00:00
);
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;
}
}
}