80 lines
2.5 KiB
Dart
80 lines
2.5 KiB
Dart
import 'package:real_estate_mobile/core/network/api_client.dart';
|
|
|
|
class _CachedUrl {
|
|
final String url;
|
|
final DateTime cachedAt;
|
|
_CachedUrl(this.url) : cachedAt = DateTime.now();
|
|
|
|
/// Presigned URLs typically expire in 1 hour; refresh after 45 minutes.
|
|
bool get isExpired =>
|
|
DateTime.now().difference(cachedAt).inMinutes >= 45;
|
|
}
|
|
|
|
/// Resolves S3 keys to presigned download URLs via the backend API.
|
|
/// Caches results in memory with expiry to handle presigned URL expiration.
|
|
class ImageUrlResolver {
|
|
ImageUrlResolver._();
|
|
static final ImageUrlResolver instance = ImageUrlResolver._();
|
|
|
|
final Map<String, _CachedUrl> _cache = {};
|
|
final Map<String, Future<String?>> _pending = {};
|
|
|
|
/// Returns a direct image URL.
|
|
/// - If [imageUrl] is already an http(s) URL, returns it as-is.
|
|
/// - If it's an S3 key, fetches a presigned download URL from the backend.
|
|
Future<String?> resolve(String? imageUrl) async {
|
|
if (imageUrl == null || imageUrl.isEmpty) return null;
|
|
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
|
return imageUrl;
|
|
}
|
|
|
|
// Check cache — only use if not expired
|
|
final cached = _cache[imageUrl];
|
|
if (cached != null && !cached.isExpired) return cached.url;
|
|
|
|
// Deduplicate in-flight requests for the same key
|
|
if (_pending.containsKey(imageUrl)) return _pending[imageUrl];
|
|
|
|
final future = _fetchPresignedUrl(imageUrl);
|
|
_pending[imageUrl] = future;
|
|
|
|
try {
|
|
final result = await future;
|
|
if (result != null) _cache[imageUrl] = _CachedUrl(result);
|
|
return result;
|
|
} finally {
|
|
_pending.remove(imageUrl);
|
|
}
|
|
}
|
|
|
|
Future<String?> _fetchPresignedUrl(String key) async {
|
|
try {
|
|
final response = await ApiClient.instance.dio.get(
|
|
'/upload/presigned-download-url',
|
|
queryParameters: {'key': key},
|
|
);
|
|
final data = response.data;
|
|
// Backend wraps: { success: true, data: { url: "..." } }
|
|
if (data is Map<String, dynamic>) {
|
|
final payload = data['data'];
|
|
if (payload is Map<String, dynamic>) {
|
|
return payload['url'] as String?;
|
|
}
|
|
// In case response isn't wrapped (direct controller return)
|
|
return data['url'] as String?;
|
|
}
|
|
return null;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void clearCache() => _cache.clear();
|
|
|
|
/// Evict a single key from the in-memory cache so the next resolve
|
|
/// fetches a fresh presigned URL.
|
|
void evict(String? key) {
|
|
if (key != null) _cache.remove(key);
|
|
}
|
|
}
|