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 _maxImageBytes = 500 * 1024;
|
||||||
static const int _maxWidth = 1080;
|
static const int _maxWidth = 1080;
|
||||||
static const int _signedUrlExpirySeconds = 3600;
|
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.
|
/// Upload a car image for a specific hotwheels entry.
|
||||||
/// Returns the storage path on success (e.g. `uid/123.jpg`).
|
/// Returns the storage path on success (e.g. `uid/123.jpg`).
|
||||||
|
|
@ -40,16 +43,32 @@ class StorageService {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_signedUrlCache.remove(path);
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates a temporary signed URL for a private image path.
|
/// Generates a temporary signed URL for a private image path.
|
||||||
static Future<String?> createSignedUrl(String? path) async {
|
static Future<String?> createSignedUrl(String? path) async {
|
||||||
if (path == null || path.isEmpty) return null;
|
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 {
|
try {
|
||||||
return await supabase.storage
|
final signed = await supabase.storage
|
||||||
.from(_bucket)
|
.from(_bucket)
|
||||||
.createSignedUrl(path, _signedUrlExpirySeconds);
|
.createSignedUrl(path, _signedUrlExpirySeconds);
|
||||||
|
_signedUrlCache[path] = _SignedUrlCacheEntry(
|
||||||
|
url: signed,
|
||||||
|
expiresAt: now.add(const Duration(seconds: _signedUrlExpirySeconds)),
|
||||||
|
);
|
||||||
|
_pruneSignedUrlCache(now);
|
||||||
|
return signed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('StorageService.createSignedUrl error: $e');
|
debugPrint('StorageService.createSignedUrl error: $e');
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -59,6 +78,7 @@ class StorageService {
|
||||||
/// Deletes an image using its storage path.
|
/// Deletes an image using its storage path.
|
||||||
static Future<void> deleteCarImage(String? path) async {
|
static Future<void> deleteCarImage(String? path) async {
|
||||||
if (path == null || path.isEmpty) return;
|
if (path == null || path.isEmpty) return;
|
||||||
|
_signedUrlCache.remove(path);
|
||||||
try {
|
try {
|
||||||
await supabase.storage.from(_bucket).remove([path]);
|
await supabase.storage.from(_bucket).remove([path]);
|
||||||
} catch (e) {
|
} 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 {
|
static Future<Uint8List> _compressImage(File source) async {
|
||||||
final bytes = await source.readAsBytes();
|
final bytes = await source.readAsBytes();
|
||||||
final decoded = img.decodeImage(bytes);
|
final decoded = img.decodeImage(bytes);
|
||||||
|
|
@ -102,3 +138,10 @@ class StorageService {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SignedUrlCacheEntry {
|
||||||
|
final String url;
|
||||||
|
final DateTime expiresAt;
|
||||||
|
|
||||||
|
const _SignedUrlCacheEntry({required this.url, required this.expiresAt});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue