176 lines
5.2 KiB
Dart
176 lines
5.2 KiB
Dart
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 {
|
||
final id = objectId ?? _uuid.v4();
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|