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 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 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 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 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 fileExists({ required String bucket, required String objectId, }) async { try { await _client.statObject(bucket, objectId); return true; } catch (e) { return false; } } }