fix: improve image loading for cards with empty or invalid image fields
- Add fallback image search when DB image field is empty - Add detailed logging for image resolution diagnostics - Allow image loading even when image field is not set - Try to find images by cardId when normal resolution fails - Opportunistic DB migration when resolving legacy image references
This commit is contained in:
parent
a50fb4110a
commit
b45153975a
2 changed files with 112 additions and 58 deletions
|
|
@ -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,44 +419,49 @@ 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) {
|
||||
if (resolved == null) {
|
||||
print('Could not resolve image file for card $cardId');
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -463,34 +472,6 @@ class PacksApiV2 {
|
|||
'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) {
|
||||
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');
|
||||
}
|
||||
final bytes = await file.readAsBytes();
|
||||
|
||||
return Response.ok(
|
||||
bytes,
|
||||
headers: {
|
||||
'Content-Type': stored.contentType,
|
||||
'Cache-Control': 'public, max-age=86400', // Cache for 1 day
|
||||
},
|
||||
);
|
||||
} catch (e, s) {
|
||||
print('Error fetching card image: $e\n$s');
|
||||
return _internalServerError('Error loading image');
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
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)) {
|
||||
return _tryFileNameFallbackByBaseName(
|
||||
print('Image value is UUID, trying baseName fallback');
|
||||
final result = await _tryFileNameFallbackByBaseName(
|
||||
assetsDir: assetsDir,
|
||||
baseName: v,
|
||||
isBack: isBack,
|
||||
);
|
||||
if (result != null) return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 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<ResolvedCardImage?> _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<File>().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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue