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,
|
String cardId,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
|
print('getCardImage called: packId=$packId, cardId=$cardId');
|
||||||
|
|
||||||
if (cardId.isEmpty) {
|
if (cardId.isEmpty) {
|
||||||
|
print('Invalid card ID: empty');
|
||||||
return _badRequest('Invalid card ID');
|
return _badRequest('Invalid card ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packId.isEmpty) {
|
if (packId.isEmpty) {
|
||||||
|
print('Invalid pack ID: empty');
|
||||||
return _badRequest('Invalid pack ID');
|
return _badRequest('Invalid pack ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -415,44 +419,49 @@ class PacksApiV2 {
|
||||||
// We allow access to images for enabled packs even without auth
|
// We allow access to images for enabled packs even without auth
|
||||||
// to support image previews in public listings
|
// to support image previews in public listings
|
||||||
final pack = await _db.packDao.getPackById(packId);
|
final pack = await _db.packDao.getPackById(packId);
|
||||||
|
print('Pack lookup result: pack=$pack, enabled=${pack?.enabled}');
|
||||||
if (pack == null || !pack.enabled) {
|
if (pack == null || !pack.enabled) {
|
||||||
|
print('Pack not found or not enabled');
|
||||||
return _notFound('Pack not found or not enabled');
|
return _notFound('Pack not found or not enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get card and verify it belongs to the pack
|
// Get card and verify it belongs to the pack
|
||||||
final card = await _db.packDao.getCardById(cardId);
|
final card = await _db.packDao.getCardById(cardId);
|
||||||
if (card == null || card.image.isEmpty) {
|
print('Card lookup result: card=$card, image=${card?.image}');
|
||||||
return _notFound('Card or image not found');
|
if (card == null) {
|
||||||
|
print('Card not found');
|
||||||
|
return _notFound('Card not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify card belongs to this pack
|
// Verify card belongs to this pack
|
||||||
final packCards = await _db.packDao.getPackCards(packId);
|
final packCards = await _db.packDao.getPackCards(packId);
|
||||||
|
print('Pack cards count: ${packCards.length}');
|
||||||
final belongsToPack = packCards.any((c) => c.id == cardId);
|
final belongsToPack = packCards.any((c) => c.id == cardId);
|
||||||
|
print('Card belongs to pack: $belongsToPack');
|
||||||
if (!belongsToPack) {
|
if (!belongsToPack) {
|
||||||
|
print('Card does not belong to this pack');
|
||||||
return _notFound('Card does not belong to this pack');
|
return _notFound('Card does not belong to this pack');
|
||||||
}
|
}
|
||||||
|
|
||||||
final imageValue = card.image.trim();
|
final imageValue = card.image.trim();
|
||||||
if (imageValue.isEmpty) {
|
print('Image value after trim: "$imageValue"');
|
||||||
return _notFound('Image not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote image: redirect (DB stores a path/URL)
|
// Try to resolve image (now handles empty imageValue with fallback)
|
||||||
if (CardImageStorage.isRemoteUrl(imageValue)) {
|
|
||||||
return Response.found(imageValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local image file name (preferred), or legacy values (API URL / UUID / base64)
|
|
||||||
final resolved = await CardImageStorage.tryResolveLocalFile(
|
final resolved = await CardImageStorage.tryResolveLocalFile(
|
||||||
cardId: cardId,
|
cardId: cardId,
|
||||||
imageValue: imageValue,
|
imageValue: imageValue,
|
||||||
isBack: false,
|
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,
|
// Opportunistic migration: if DB accidentally contains an API URL/UUID,
|
||||||
// rewrite to the real file name once we successfully resolve it.
|
// rewrite to the real file name once we successfully resolve it.
|
||||||
if (resolved.fileName != card.image) {
|
if (resolved.fileName != card.image) {
|
||||||
|
print('Migrating image field from "${card.image}" to "${resolved.fileName}"');
|
||||||
await _db.packDao.updateCard(card.copyWith(image: 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
|
'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) {
|
} catch (e, s) {
|
||||||
print('Error fetching card image: $e\n$s');
|
print('Error fetching card image: $e\n$s');
|
||||||
return _internalServerError('Error loading image');
|
return _internalServerError('Error loading image');
|
||||||
|
|
|
||||||
|
|
@ -217,40 +217,65 @@ class CardImageStorage {
|
||||||
Directory? assetsDirectory,
|
Directory? assetsDirectory,
|
||||||
}) async {
|
}) async {
|
||||||
final assetsDir = _assetsDirectory(assetsDirectory);
|
final assetsDir = _assetsDirectory(assetsDirectory);
|
||||||
|
print('tryResolveLocalFile: cardId=$cardId, imageValue="$imageValue", isBack=$isBack, assetsDir=${assetsDir.path}');
|
||||||
final v = imageValue.trim();
|
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);
|
final sanitized = sanitizeCardsFileName(v);
|
||||||
|
print('Sanitized filename: "$sanitized"');
|
||||||
if (sanitized != null) {
|
if (sanitized != null) {
|
||||||
final file = File('${assetsDir.path}/cards/$sanitized');
|
final file = File('${assetsDir.path}/cards/$sanitized');
|
||||||
|
print('Checking file: ${file.path}, exists: ${file.existsSync()}');
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
final bytes = await file.readAsBytes();
|
final bytes = await file.readAsBytes();
|
||||||
final contentType =
|
final contentType =
|
||||||
_detectContentType(bytes) ?? _contentTypeFromFileName(sanitized);
|
_detectContentType(bytes) ?? _contentTypeFromFileName(sanitized);
|
||||||
|
print('File found and loaded successfully');
|
||||||
return ResolvedCardImage(
|
return ResolvedCardImage(
|
||||||
fileName: sanitized,
|
fileName: sanitized,
|
||||||
bytes: bytes,
|
bytes: bytes,
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
print('File does not exist');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Self-referential API URL (common admin UI round-trip bug).
|
// Self-referential API URL (common admin UI round-trip bug).
|
||||||
if (isApiImageUrl(v)) {
|
if (isApiImageUrl(v)) {
|
||||||
return _tryCardIdFallback(
|
print('Image value is API URL, trying cardId fallback');
|
||||||
|
final result = await _tryCardIdFallback(
|
||||||
assetsDir: assetsDir,
|
assetsDir: assetsDir,
|
||||||
cardId: cardId,
|
cardId: cardId,
|
||||||
isBack: isBack,
|
isBack: isBack,
|
||||||
);
|
);
|
||||||
|
if (result != null) return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UUID without extension.
|
// UUID without extension.
|
||||||
if (_uuidRegex.hasMatch(v)) {
|
if (_uuidRegex.hasMatch(v)) {
|
||||||
return _tryFileNameFallbackByBaseName(
|
print('Image value is UUID, trying baseName fallback');
|
||||||
|
final result = await _tryFileNameFallbackByBaseName(
|
||||||
assetsDir: assetsDir,
|
assetsDir: assetsDir,
|
||||||
baseName: v,
|
baseName: v,
|
||||||
isBack: isBack,
|
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;
|
return null;
|
||||||
|
|
@ -306,6 +331,54 @@ class CardImageStorage {
|
||||||
return null;
|
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) {
|
static String _contentTypeFromFileName(String fileName) {
|
||||||
final lower = fileName.toLowerCase();
|
final lower = fileName.toLowerCase();
|
||||||
if (lower.endsWith('.webp')) return 'image/webp';
|
if (lower.endsWith('.webp')) return 'image/webp';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue