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 { final id = objectId ?? _uuid.v4(); try { await _client.putObject( bucket, id, Stream.value(bytes), size: bytes.length, metadata: {'Content-Type': contentType}, ); print('✅ Uploaded file to MinIO: $bucket/$id (${bytes.length} bytes)'); 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; } } }