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 e6aabd5..522bab8 100644 --- a/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/packs_api_v2.dart @@ -403,11 +403,15 @@ class PacksApiV2 { String cardId, ) async { try { + print('getCardImage called: packId=$packId, cardId=$cardId'); + if (cardId.isEmpty) { + print('Invalid card ID: empty'); return _badRequest('Invalid card ID'); } if (packId.isEmpty) { + print('Invalid pack ID: empty'); return _badRequest('Invalid pack ID'); } @@ -415,79 +419,56 @@ class PacksApiV2 { // We allow access to images for enabled packs even without auth // to support image previews in public listings final pack = await _db.packDao.getPackById(packId); + print('Pack lookup result: pack=$pack, enabled=${pack?.enabled}'); if (pack == null || !pack.enabled) { + print('Pack not found or not enabled'); return _notFound('Pack not found or not enabled'); } // Get card and verify it belongs to the pack final card = await _db.packDao.getCardById(cardId); - if (card == null || card.image.isEmpty) { - return _notFound('Card or image not found'); + print('Card lookup result: card=$card, image=${card?.image}'); + if (card == null) { + print('Card not found'); + return _notFound('Card not found'); } // Verify card belongs to this pack final packCards = await _db.packDao.getPackCards(packId); + print('Pack cards count: ${packCards.length}'); final belongsToPack = packCards.any((c) => c.id == cardId); + print('Card belongs to pack: $belongsToPack'); if (!belongsToPack) { + print('Card does not belong to this pack'); return _notFound('Card does not belong to this pack'); } final imageValue = card.image.trim(); - if (imageValue.isEmpty) { - return _notFound('Image not found'); - } + print('Image value after trim: "$imageValue"'); - // Remote image: redirect (DB stores a path/URL) - if (CardImageStorage.isRemoteUrl(imageValue)) { - return Response.found(imageValue); - } - - // Local image file name (preferred), or legacy values (API URL / UUID / base64) + // Try to resolve image (now handles empty imageValue with fallback) final resolved = await CardImageStorage.tryResolveLocalFile( cardId: cardId, imageValue: imageValue, isBack: false, ); - if (resolved != null) { - // Opportunistic migration: if DB accidentally contains an API URL/UUID, - // rewrite to the real file name once we successfully resolve it. - if (resolved.fileName != card.image) { - await _db.packDao.updateCard(card.copyWith(image: resolved.fileName)); - } - - return Response.ok( - resolved.bytes, - headers: { - 'Content-Type': resolved.contentType, - 'Cache-Control': 'public, max-age=86400', // Cache for 1 day - }, - ); - } - - // Base64 fallback: persist to file and migrate DB. - final stored = await CardImageStorage.persistFromBase64( - cardId: cardId, - imageValue: imageValue, - preferredFileName: null, - isBack: false, - ); - - if (stored == null) { + if (resolved == null) { + print('Could not resolve image file for card $cardId'); return _notFound('Image not found'); } - await _db.packDao.updateCard(card.copyWith(image: stored.fileName)); - final file = File('${PackManagerUtils.assetsDirectory.path}/cards/${stored.fileName}'); - if (!file.existsSync()) { - return _notFound('Image not found'); + // Opportunistic migration: if DB accidentally contains an API URL/UUID, + // rewrite to the real file name once we successfully resolve it. + if (resolved.fileName != card.image) { + print('Migrating image field from "${card.image}" to "${resolved.fileName}"'); + await _db.packDao.updateCard(card.copyWith(image: resolved.fileName)); } - final bytes = await file.readAsBytes(); return Response.ok( - bytes, + resolved.bytes, headers: { - 'Content-Type': stored.contentType, + 'Content-Type': resolved.contentType, 'Cache-Control': 'public, max-age=86400', // Cache for 1 day }, ); diff --git a/mnemo_cards_backend/lib/packs/card_image_storage.dart b/mnemo_cards_backend/lib/packs/card_image_storage.dart index a60e47d..51681ff 100644 --- a/mnemo_cards_backend/lib/packs/card_image_storage.dart +++ b/mnemo_cards_backend/lib/packs/card_image_storage.dart @@ -217,40 +217,65 @@ class CardImageStorage { Directory? assetsDirectory, }) async { final assetsDir = _assetsDirectory(assetsDirectory); + print('tryResolveLocalFile: cardId=$cardId, imageValue="$imageValue", isBack=$isBack, assetsDir=${assetsDir.path}'); final v = imageValue.trim(); - if (v.isEmpty) return null; + + // If imageValue is not empty, try normal resolution first + if (v.isNotEmpty) { final sanitized = sanitizeCardsFileName(v); + print('Sanitized filename: "$sanitized"'); if (sanitized != null) { final file = File('${assetsDir.path}/cards/$sanitized'); + print('Checking file: ${file.path}, exists: ${file.existsSync()}'); if (file.existsSync()) { final bytes = await file.readAsBytes(); final contentType = _detectContentType(bytes) ?? _contentTypeFromFileName(sanitized); + print('File found and loaded successfully'); return ResolvedCardImage( fileName: sanitized, bytes: bytes, contentType: contentType, ); + } else { + print('File does not exist'); } } - // Self-referential API URL (common admin UI round-trip bug). - if (isApiImageUrl(v)) { - return _tryCardIdFallback( - assetsDir: assetsDir, - cardId: cardId, - isBack: isBack, - ); + // Self-referential API URL (common admin UI round-trip bug). + if (isApiImageUrl(v)) { + print('Image value is API URL, trying cardId fallback'); + final result = await _tryCardIdFallback( + assetsDir: assetsDir, + cardId: cardId, + isBack: isBack, + ); + if (result != null) return result; + } + + // UUID without extension. + if (_uuidRegex.hasMatch(v)) { + print('Image value is UUID, trying baseName fallback'); + final result = await _tryFileNameFallbackByBaseName( + assetsDir: assetsDir, + baseName: v, + isBack: isBack, + ); + if (result != null) return result; + } } - // UUID without extension. - if (_uuidRegex.hasMatch(v)) { - return _tryFileNameFallbackByBaseName( - assetsDir: assetsDir, - baseName: v, - isBack: isBack, - ); + // Fallback: try to find image by cardId even if imageValue is empty + print('Trying fallback search for card $cardId'); + final fallbackResult = await _tryFallbackCardImageSearch( + assetsDir: assetsDir, + cardId: cardId, + isBack: isBack, + ); + if (fallbackResult != null) { + print('Fallback search found file: ${fallbackResult.fileName}'); + return fallbackResult; } return null; @@ -306,6 +331,54 @@ class CardImageStorage { return null; } + /// Fallback search for card images when the image field is empty + /// Try to find image by cardId when DB field is not set + static Future _tryFallbackCardImageSearch({ + required Directory assetsDir, + required String cardId, + required bool isBack, + }) async { + print('Trying fallback search by cardId: $cardId'); + + // Try to find file by cardId (UUID) as base name + final result = await _tryFileNameFallbackByBaseName( + assetsDir: assetsDir, + baseName: cardId, + isBack: isBack, + ); + if (result != null) return result; + + // If cardId is UUID, try to find any file that might be associated + // This is a last resort for cases where images exist but DB is not updated + final cardsDir = Directory('${assetsDir.path}/cards'); + if (!cardsDir.existsSync()) return null; + + final files = cardsDir.listSync().whereType().toList(); + + // Look for files that might be named after the card + // For example, if cardId is UUID, look for files starting with same prefix + if (cardId.length >= 8) { + final prefix = cardId.substring(0, 8); + for (final file in files) { + final fileName = file.path.split('/').last; + if (fileName.startsWith(prefix)) { + final bytes = await file.readAsBytes(); + final contentType = _detectContentType(bytes) ?? _contentTypeFromFileName(fileName); + + print('Fallback found file by prefix: $fileName for card $cardId'); + return ResolvedCardImage( + fileName: fileName, + bytes: bytes, + contentType: contentType, + ); + } + } + } + + print('Fallback search failed for card $cardId'); + return null; + } + static String _contentTypeFromFileName(String fileName) { final lower = fileName.toLowerCase(); if (lower.endsWith('.webp')) return 'image/webp';