minio fix
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (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

This commit is contained in:
Dmitry 2026-01-08 21:17:57 +03:00
parent 9ed1a00a38
commit 118f0e2ede
4 changed files with 119 additions and 41 deletions

View file

@ -4,18 +4,30 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart';
/// Extension для конвертации GameCard в GameCardDto для admin API /// Extension для конвертации GameCard в GameCardDto для admin API
extension GameCardAdminExtension on GameCard { extension GameCardAdminExtension on GameCard {
/// Конвертация GameCard в GameCardDto с учетом packId и конвертации изображений /// Конвертация GameCard в GameCardDto с учетом packId и конвертации изображений
///
/// DEPRECATED: Use toGameCardDtoWithPresignedUrls instead
/// This method is kept for backwards compatibility
///
/// image/imageBack always contains UUID (objectId in MinIO)
/// imageUrl/imageBackUrl will contain URLs from convertImageToUrl
GameCardDto toGameCardDtoWithPack( GameCardDto toGameCardDtoWithPack(
String? packId, String? packId,
String? Function(String?, String?, String) convertImageToUrl, String? Function(String?, String?, String) convertImageToUrl,
String? Function(String?, String?, String) convertImageBackToUrl, String? Function(String?, String?, String) convertImageBackToUrl,
) { ) {
// Generate URLs from UUIDs
final imageUrl = convertImageToUrl(image, packId, id);
final imageBackUrl = convertImageBackToUrl(imageBack, packId, id);
return GameCardDto( return GameCardDto(
id: id, id: id,
original: original, original: original,
translation: translation, translation: translation,
mnemo: mnemo ?? '', mnemo: mnemo ?? '',
image: convertImageToUrl(image, packId, id), image: image, // Keep UUID in image field
imageBack: convertImageBackToUrl(imageBack, packId, id), imageUrl: imageUrl, // Set URL in imageUrl
imageBack: imageBack, // Keep UUID in imageBack field
imageBackUrl: imageBackUrl, // Set URL in imageBackUrl
back: back, back: back,
transcription: transcription ?? '', transcription: transcription ?? '',
transcriptionMnemo: transcriptionMnemo, transcriptionMnemo: transcriptionMnemo,
@ -23,19 +35,27 @@ extension GameCardAdminExtension on GameCard {
} }
/// Асинхронная конвертация GameCard в GameCardDto с генерацией presigned URLs /// Асинхронная конвертация GameCard в GameCardDto с генерацией presigned URLs
///
/// image/imageBack always contains UUID (objectId in MinIO)
/// imageUrl/imageBackUrl will contain presigned URLs
Future<GameCardDto> toGameCardDtoWithPresignedUrls( Future<GameCardDto> toGameCardDtoWithPresignedUrls(
String? packId, String? packId,
Future<String?> Function(String?, String?, String) convertImageToUrl, Future<String?> Function(String?, String?, String) convertImageToUrl,
Future<String?> Function(String?, String?, String) convertImageBackToUrl, Future<String?> Function(String?, String?, String) convertImageBackToUrl,
) async { ) async {
// Generate presigned URLs
final imageUrl = await convertImageToUrl(image, packId, id);
final imageBackUrl = await convertImageBackToUrl(imageBack, packId, id);
return GameCardDto( return GameCardDto(
id: id, id: id,
original: original, original: original,
translation: translation, translation: translation,
mnemo: mnemo ?? '', mnemo: mnemo ?? '',
image: await convertImageToUrl(image, packId, id), image: image, // Keep UUID in image field
imageBack: await convertImageBackToUrl(imageBack, packId, id), imageUrl: imageUrl, // Set presigned URL in imageUrl
back: back, imageBack: imageBack, // Keep UUID in imageBack field
imageBackUrl: imageBackUrl, // Set presigned URL in imageBackUrl
transcription: transcription ?? '', transcription: transcription ?? '',
transcriptionMnemo: transcriptionMnemo, transcriptionMnemo: transcriptionMnemo,
); );

View file

@ -278,8 +278,36 @@ class PacksApiV2 {
final packDto = await _packManager.getPackDto(packId, user); final packDto = await _packManager.getPackDto(packId, user);
// Generate presigned URLs for all cards in the pack
// image/imageBack always contains UUID (objectId in MinIO)
// imageUrl/imageBackUrl will contain presigned URLs
// Presigned URLs now have 7 days expiration to avoid 403 errors on cached data
final cardsWithPresignedUrls = await Future.wait(
packDto.cards.map((card) async {
// Generate presigned URLs for images if they are object IDs (UUIDs)
final imageUrl = await _getPresignedUrlIfUuid(
card.image,
MinioConfig.cardImagesBucket,
);
final imageBackUrl = await _getPresignedUrlIfUuid(
card.imageBack,
MinioConfig.cardImagesBucket,
);
// Return card with presigned URLs in imageUrl/imageBackUrl
// Keep original UUID in image/imageBack
return card.copyWith(
imageUrl: imageUrl, // Set presigned URL in imageUrl
imageBackUrl: imageBackUrl, // Set presigned URL in imageBackUrl
);
}),
);
// Create updated pack DTO with cards that have presigned URLs
final updatedPackDto = packDto.copyWith(cards: cardsWithPresignedUrls);
// Include purchase status in response if user is authenticated // Include purchase status in response if user is authenticated
final packJson = packDto.toJson(); final packJson = updatedPackDto.toJson();
if (user != null) { if (user != null) {
final isAvailable = await _productAvailabilityManager.isPackAvailable( final isAvailable = await _productAvailabilityManager.isPackAvailable(
@ -321,7 +349,35 @@ class PacksApiV2 {
// Return pack preview as buy page // Return pack preview as buy page
final packDto = await _packManager.getPackDto(packId, user); final packDto = await _packManager.getPackDto(packId, user);
final packJson = packDto.toJson();
// Generate presigned URLs for all cards in the pack
// image/imageBack always contains UUID (objectId in MinIO)
// imageUrl/imageBackUrl will contain presigned URLs
// Presigned URLs now have 7 days expiration to avoid 403 errors on cached data
final cardsWithPresignedUrls = await Future.wait(
packDto.cards.map((card) async {
// Generate presigned URLs for images if they are object IDs (UUIDs)
final imageUrl = await _getPresignedUrlIfUuid(
card.image,
MinioConfig.cardImagesBucket,
);
final imageBackUrl = await _getPresignedUrlIfUuid(
card.imageBack,
MinioConfig.cardImagesBucket,
);
// Return card with presigned URLs in imageUrl/imageBackUrl
// Keep original UUID in image/imageBack
return card.copyWith(
imageUrl: imageUrl, // Set presigned URL in imageUrl
imageBackUrl: imageBackUrl, // Set presigned URL in imageBackUrl
);
}),
);
// Create updated pack DTO with cards that have presigned URLs
final updatedPackDto = packDto.copyWith(cards: cardsWithPresignedUrls);
final packJson = updatedPackDto.toJson();
// Include purchase status in response if user is authenticated // Include purchase status in response if user is authenticated
if (user != null) { if (user != null) {

View file

@ -11,11 +11,13 @@ import '../../utils/card_image_utils.dart';
/// Widget that handles expired presigned URLs by automatically refreshing them /// Widget that handles expired presigned URLs by automatically refreshing them
/// ///
/// image/imageBack always contains UUID (objectId in MinIO)
/// imageUrl/imageBackUrl always contains presigned URL
///
/// When a presigned URL expires (403 error), this widget: /// When a presigned URL expires (403 error), this widget:
/// 1. Detects the error /// 1. Detects the error
/// 2. Checks if card.image contains an objectId (UUID) /// 2. Uses card.image (UUID) to request a new presigned URL from the API
/// 3. Requests a new presigned URL from the API /// 3. Updates the image URL and retries loading
/// 4. Updates the image URL and retries loading
class ExpiringCardImage extends StatefulWidget { class ExpiringCardImage extends StatefulWidget {
const ExpiringCardImage({ const ExpiringCardImage({
super.key, super.key,
@ -81,10 +83,26 @@ class _ExpiringCardImageState extends State<ExpiringCardImage> {
} }
String? _getObjectId() { String? _getObjectId() {
if (widget.isBackImage) { // image/imageBack always contains UUID (objectId in MinIO)
return widget.card.imageBack; // imageUrl/imageBackUrl always contains presigned URL
final imageValue = widget.isBackImage
? widget.card.imageBack
: widget.card.image;
if (imageValue == null || imageValue.isEmpty) {
return null;
} }
return widget.card.image;
// Verify it's a valid UUID
if (!_isUuid(imageValue)) {
log(
'Warning: image field does not contain valid UUID: $imageValue',
name: 'ExpiringCardImage',
);
return null;
}
return imageValue;
} }
bool _isUuid(String? value) { bool _isUuid(String? value) {

View file

@ -5,27 +5,19 @@ import '../domain/config/api_config_v2.dart';
class CardImageUtils { class CardImageUtils {
/// Get the display URL for a card's image /// Get the display URL for a card's image
/// ///
/// Priority: /// image always contains UUID (objectId in MinIO)
/// 1. imageUrl (presigned URL from backend) /// imageUrl always contains presigned URL
/// 2. image if it's a full URL (starts with http/https)
/// 3. image if it's a filename - convert to API endpoint URL
/// ///
/// Returns imageUrl if available, otherwise falls back to API endpoint
/// Returns null if no image is available /// Returns null if no image is available
static String? getCardImageUrl(GameCardDto card, String packId) { static String? getCardImageUrl(GameCardDto card, String packId) {
// First priority: use presigned URL if available // Use presigned URL if available (always preferred)
if (card.imageUrl != null && card.imageUrl!.isNotEmpty) { if (card.imageUrl != null && card.imageUrl!.isNotEmpty) {
return card.imageUrl; return card.imageUrl;
} }
// Second priority: check if image is a full URL // Fallback: use API endpoint if image (UUID) is available
final image = card.image; if (card.image != null && card.image!.isNotEmpty) {
if (image != null && image.isNotEmpty) {
// If it's already a full URL, use it
if (image.startsWith('http://') || image.startsWith('https://')) {
return image;
}
// If it's a filename, convert to API endpoint URL
return ApiConfigV2.getCardImageUrl(packId, card.id); return ApiConfigV2.getCardImageUrl(packId, card.id);
} }
@ -34,27 +26,19 @@ class CardImageUtils {
/// Get the display URL for a card's back image /// Get the display URL for a card's back image
/// ///
/// Priority: /// imageBack always contains UUID (objectId in MinIO)
/// 1. imageBackUrl (presigned URL from backend) /// imageBackUrl always contains presigned URL
/// 2. imageBack if it's a full URL (starts with http/https)
/// 3. imageBack if it's a filename - convert to API endpoint URL
/// ///
/// Returns imageBackUrl if available, otherwise falls back to API endpoint
/// Returns null if no back image is available /// Returns null if no back image is available
static String? getCardImageBackUrl(GameCardDto card, String packId) { static String? getCardImageBackUrl(GameCardDto card, String packId) {
// First priority: use presigned URL if available // Use presigned URL if available (always preferred)
if (card.imageBackUrl != null && card.imageBackUrl!.isNotEmpty) { if (card.imageBackUrl != null && card.imageBackUrl!.isNotEmpty) {
return card.imageBackUrl; return card.imageBackUrl;
} }
// Second priority: check if imageBack is a full URL // Fallback: use API endpoint if imageBack (UUID) is available
final imageBack = card.imageBack; if (card.imageBack != null && card.imageBack!.isNotEmpty) {
if (imageBack != null && imageBack.isNotEmpty) {
// If it's already a full URL, use it
if (imageBack.startsWith('http://') || imageBack.startsWith('https://')) {
return imageBack;
}
// If it's a filename, convert to API endpoint URL
return ApiConfigV2.getCardImageBackUrl(packId, card.id); return ApiConfigV2.getCardImageBackUrl(packId, card.id);
} }