From 118f0e2ede816bd500fef228f9578ff659450b32 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 8 Jan 2026 21:17:57 +0300 Subject: [PATCH] minio fix --- .../v2/extensions/game_card_extensions.dart | 30 ++++++++-- .../lib/api/v2/packs_api_v2.dart | 60 ++++++++++++++++++- .../widgets/expiring_card_image.dart | 30 ++++++++-- .../lib/utils/card_image_utils.dart | 40 ++++--------- 4 files changed, 119 insertions(+), 41 deletions(-) diff --git a/mnemo_cards_backend/lib/api/v2/extensions/game_card_extensions.dart b/mnemo_cards_backend/lib/api/v2/extensions/game_card_extensions.dart index 572846a..05cb080 100644 --- a/mnemo_cards_backend/lib/api/v2/extensions/game_card_extensions.dart +++ b/mnemo_cards_backend/lib/api/v2/extensions/game_card_extensions.dart @@ -4,18 +4,30 @@ import 'package:mnemo_cards_common/mnemo_cards_common.dart'; /// Extension для конвертации GameCard в GameCardDto для admin API extension GameCardAdminExtension on GameCard { /// Конвертация 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( String? packId, String? Function(String?, String?, String) convertImageToUrl, String? Function(String?, String?, String) convertImageBackToUrl, ) { + // Generate URLs from UUIDs + final imageUrl = convertImageToUrl(image, packId, id); + final imageBackUrl = convertImageBackToUrl(imageBack, packId, id); + return GameCardDto( id: id, original: original, translation: translation, mnemo: mnemo ?? '', - image: convertImageToUrl(image, packId, id), - imageBack: convertImageBackToUrl(imageBack, packId, id), + image: image, // Keep UUID in image field + imageUrl: imageUrl, // Set URL in imageUrl + imageBack: imageBack, // Keep UUID in imageBack field + imageBackUrl: imageBackUrl, // Set URL in imageBackUrl back: back, transcription: transcription ?? '', transcriptionMnemo: transcriptionMnemo, @@ -23,19 +35,27 @@ extension GameCardAdminExtension on GameCard { } /// Асинхронная конвертация GameCard в GameCardDto с генерацией presigned URLs + /// + /// image/imageBack always contains UUID (objectId in MinIO) + /// imageUrl/imageBackUrl will contain presigned URLs Future toGameCardDtoWithPresignedUrls( String? packId, Future Function(String?, String?, String) convertImageToUrl, Future Function(String?, String?, String) convertImageBackToUrl, ) async { + // Generate presigned URLs + final imageUrl = await convertImageToUrl(image, packId, id); + final imageBackUrl = await convertImageBackToUrl(imageBack, packId, id); + return GameCardDto( id: id, original: original, translation: translation, mnemo: mnemo ?? '', - image: await convertImageToUrl(image, packId, id), - imageBack: await convertImageBackToUrl(imageBack, packId, id), - back: back, + image: image, // Keep UUID in image field + imageUrl: imageUrl, // Set presigned URL in imageUrl + imageBack: imageBack, // Keep UUID in imageBack field + imageBackUrl: imageBackUrl, // Set presigned URL in imageBackUrl transcription: transcription ?? '', transcriptionMnemo: transcriptionMnemo, ); diff --git a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart index 9fa8f82..6ed650a 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -278,8 +278,36 @@ class PacksApiV2 { 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 - final packJson = packDto.toJson(); + final packJson = updatedPackDto.toJson(); if (user != null) { final isAvailable = await _productAvailabilityManager.isPackAvailable( @@ -321,7 +349,35 @@ class PacksApiV2 { // Return pack preview as buy page 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 if (user != null) { diff --git a/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart b/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart index a23ced4..e2c4059 100644 --- a/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart +++ b/mnemo_cards_web_v2/lib/presentation/widgets/expiring_card_image.dart @@ -11,11 +11,13 @@ import '../../utils/card_image_utils.dart'; /// 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: /// 1. Detects the error -/// 2. Checks if card.image contains an objectId (UUID) -/// 3. Requests a new presigned URL from the API -/// 4. Updates the image URL and retries loading +/// 2. Uses card.image (UUID) to request a new presigned URL from the API +/// 3. Updates the image URL and retries loading class ExpiringCardImage extends StatefulWidget { const ExpiringCardImage({ super.key, @@ -81,10 +83,26 @@ class _ExpiringCardImageState extends State { } String? _getObjectId() { - if (widget.isBackImage) { - return widget.card.imageBack; + // image/imageBack always contains UUID (objectId in MinIO) + // 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) { diff --git a/mnemo_cards_web_v2/lib/utils/card_image_utils.dart b/mnemo_cards_web_v2/lib/utils/card_image_utils.dart index 6fc3e7b..8081797 100644 --- a/mnemo_cards_web_v2/lib/utils/card_image_utils.dart +++ b/mnemo_cards_web_v2/lib/utils/card_image_utils.dart @@ -5,27 +5,19 @@ import '../domain/config/api_config_v2.dart'; class CardImageUtils { /// Get the display URL for a card's image /// - /// Priority: - /// 1. imageUrl (presigned URL from backend) - /// 2. image if it's a full URL (starts with http/https) - /// 3. image if it's a filename - convert to API endpoint URL + /// image always contains UUID (objectId in MinIO) + /// imageUrl always contains presigned URL /// + /// Returns imageUrl if available, otherwise falls back to API endpoint /// Returns null if no image is available 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) { return card.imageUrl; } - // Second priority: check if image is a full URL - final image = card.image; - 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 + // Fallback: use API endpoint if image (UUID) is available + if (card.image != null && card.image!.isNotEmpty) { return ApiConfigV2.getCardImageUrl(packId, card.id); } @@ -34,27 +26,19 @@ class CardImageUtils { /// Get the display URL for a card's back image /// - /// Priority: - /// 1. imageBackUrl (presigned URL from backend) - /// 2. imageBack if it's a full URL (starts with http/https) - /// 3. imageBack if it's a filename - convert to API endpoint URL + /// imageBack always contains UUID (objectId in MinIO) + /// imageBackUrl always contains presigned URL /// + /// Returns imageBackUrl if available, otherwise falls back to API endpoint /// Returns null if no back image is available 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) { return card.imageBackUrl; } - // Second priority: check if imageBack is a full URL - final imageBack = card.imageBack; - 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 + // Fallback: use API endpoint if imageBack (UUID) is available + if (card.imageBack != null && card.imageBack!.isNotEmpty) { return ApiConfigV2.getCardImageBackUrl(packId, card.id); }