perf: cache signed storage URLs
This commit is contained in:
parent
83344a06d5
commit
93cbf4b568
1 changed files with 44 additions and 1 deletions
|
|
@ -15,6 +15,9 @@ class StorageService {
|
|||
static const int _maxImageBytes = 500 * 1024;
|
||||
static const int _maxWidth = 1080;
|
||||
static const int _signedUrlExpirySeconds = 3600;
|
||||
static const _signedUrlRefreshBuffer = Duration(minutes: 3);
|
||||
static const _maxSignedUrlCacheEntries = 500;
|
||||
static final Map<String, _SignedUrlCacheEntry> _signedUrlCache = {};
|
||||
|
||||
/// Upload a car image for a specific hotwheels entry.
|
||||
/// Returns the storage path on success (e.g. `uid/123.jpg`).
|
||||
|
|
@ -40,16 +43,32 @@ class StorageService {
|
|||
),
|
||||
);
|
||||
|
||||
_signedUrlCache.remove(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Generates a temporary signed URL for a private image path.
|
||||
static Future<String?> createSignedUrl(String? path) async {
|
||||
if (path == null || path.isEmpty) return null;
|
||||
|
||||
final now = DateTime.now();
|
||||
final cached = _signedUrlCache[path];
|
||||
if (cached != null &&
|
||||
now.isBefore(cached.expiresAt.subtract(_signedUrlRefreshBuffer))) {
|
||||
return cached.url;
|
||||
}
|
||||
|
||||
try {
|
||||
return await supabase.storage
|
||||
final signed = await supabase.storage
|
||||
.from(_bucket)
|
||||
.createSignedUrl(path, _signedUrlExpirySeconds);
|
||||
_signedUrlCache[path] = _SignedUrlCacheEntry(
|
||||
url: signed,
|
||||
expiresAt: now.add(const Duration(seconds: _signedUrlExpirySeconds)),
|
||||
);
|
||||
_pruneSignedUrlCache(now);
|
||||
return signed;
|
||||
} catch (e) {
|
||||
debugPrint('StorageService.createSignedUrl error: $e');
|
||||
return null;
|
||||
|
|
@ -59,6 +78,7 @@ class StorageService {
|
|||
/// Deletes an image using its storage path.
|
||||
static Future<void> deleteCarImage(String? path) async {
|
||||
if (path == null || path.isEmpty) return;
|
||||
_signedUrlCache.remove(path);
|
||||
try {
|
||||
await supabase.storage.from(_bucket).remove([path]);
|
||||
} catch (e) {
|
||||
|
|
@ -66,6 +86,22 @@ class StorageService {
|
|||
}
|
||||
}
|
||||
|
||||
static void invalidateSignedUrl(String? path) {
|
||||
if (path == null || path.isEmpty) return;
|
||||
_signedUrlCache.remove(path);
|
||||
}
|
||||
|
||||
static void _pruneSignedUrlCache(DateTime now) {
|
||||
_signedUrlCache.removeWhere((_, entry) => now.isAfter(entry.expiresAt));
|
||||
|
||||
if (_signedUrlCache.length <= _maxSignedUrlCacheEntries) return;
|
||||
final overflow = _signedUrlCache.length - _maxSignedUrlCacheEntries;
|
||||
final keys = _signedUrlCache.keys.take(overflow).toList();
|
||||
for (final key in keys) {
|
||||
_signedUrlCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Uint8List> _compressImage(File source) async {
|
||||
final bytes = await source.readAsBytes();
|
||||
final decoded = img.decodeImage(bytes);
|
||||
|
|
@ -102,3 +138,10 @@ class StorageService {
|
|||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
class _SignedUrlCacheEntry {
|
||||
final String url;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const _SignedUrlCacheEntry({required this.url, required this.expiresAt});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue