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 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<GameCardDto> toGameCardDtoWithPresignedUrls(
String? packId,
Future<String?> Function(String?, String?, String) convertImageToUrl,
Future<String?> 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,
);

View file

@ -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) {

View file

@ -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<ExpiringCardImage> {
}
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) {

View file

@ -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);
}