Skip to content

Commit 4acd0a0

Browse files
authored
feat(storage): add purgeCache to invalidate CDN cache for a single object (#1607)
## Summary Adds `StorageFileApi.purgeCache(path, {transformations})` to invalidate the CDN cache for a single object in a bucket. Issues `DELETE /cdn/{bucket}/{path}` against the storage base URL and returns the server's success message. When `transformations` is `true`, only the resized/formatted variants are purged, leaving the original cached object intact; otherwise the object cache is purged. Requires the service-role key and the tenant `purgeCache` feature enabled on the storage server. **Outcome:** implemented **Reference:** supabase-js PR [#2429](supabase/supabase-js#2429) (`storage.file_buckets.purge_cache`) ## Compliance matrix - `storage.file_buckets.purge_cache` → `implemented` (symbol `StorageFileApi.purgeCache`) ## Tests Added unit tests in `packages/storage_client/test/basic_test.dart` covering the DELETE endpoint/URL and the `transformations` query parameter. SDK-1331
1 parent 42068e4 commit 4acd0a0

4 files changed

Lines changed: 128 additions & 0 deletions

File tree

packages/storage_client/lib/src/storage_bucket_api.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,35 @@ class StorageBucketApi {
134134
return (response as Map<String, dynamic>)['message'] as String;
135135
}
136136

137+
/// Purges the CDN cache for an entire bucket.
138+
///
139+
/// Invalidates the CDN cache for every object in the bucket [id]. Maps to
140+
/// `DELETE /cdn/{bucket}` on the storage server.
141+
///
142+
/// When [transformations] is `true`, only the resized/formatted variants are
143+
/// purged, leaving the original cached files intact. When omitted the bucket
144+
/// cache is purged.
145+
///
146+
/// Requires the service-role key and the tenant `purgeCache` feature to be
147+
/// enabled on the storage server.
148+
Future<String> purgeBucketCache(
149+
String id, {
150+
bool transformations = false,
151+
}) async {
152+
var requestUrl = Uri.parse('$url/cdn/$id');
153+
if (transformations) {
154+
requestUrl = requestUrl.replace(
155+
queryParameters: {'transformations': 'true'},
156+
);
157+
}
158+
final response = await storageFetch.delete(
159+
requestUrl.toString(),
160+
{},
161+
options: FetchOptions(headers),
162+
);
163+
return (response as Map<String, dynamic>)['message'] as String;
164+
}
165+
137166
/// Creates a new analytics bucket backed by the Apache Iceberg table format.
138167
///
139168
/// [id] is the unique identifier for the bucket you are creating.

packages/storage_client/lib/src/storage_file_api.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,37 @@ class StorageFileApi {
695695
return fileObjects;
696696
}
697697

698+
/// Purges the CDN cache for a single object.
699+
///
700+
/// Invalidates the CDN cache for the object at [path] (relative to the
701+
/// bucket). There is no wildcard or recursion; pass the exact path of the
702+
/// object to invalidate. For example: `purgeCache('folder/avatar.png')`.
703+
///
704+
/// When [transformations] is `true`, only the resized/formatted variants are
705+
/// purged, leaving the original cached file intact. When omitted the object
706+
/// cache is purged.
707+
///
708+
/// Requires the service-role key and the tenant `purgeCache` feature to be
709+
/// enabled on the storage server.
710+
Future<String> purgeCache(
711+
String path, {
712+
bool transformations = false,
713+
}) async {
714+
final finalPath = _getFinalPath(path);
715+
var requestUrl = Uri.parse('$url/cdn/$finalPath');
716+
if (transformations) {
717+
requestUrl = requestUrl.replace(
718+
queryParameters: {'transformations': 'true'},
719+
);
720+
}
721+
final response = await _storageFetch.delete(
722+
requestUrl.toString(),
723+
{},
724+
options: _fetchOptions,
725+
);
726+
return (response as Map<String, dynamic>)['message'] as String;
727+
}
728+
698729
/// Lists all the files within a bucket.
699730
///
700731
/// [path] The folder path.

packages/storage_client/test/basic_test.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,35 @@ void main() {
154154
expect(response, 'Deleted');
155155
});
156156

157+
test('should purgeBucketCache issuing DELETE to /cdn/{bucket}', () async {
158+
customHttpClient.response = {'message': 'success'};
159+
160+
final response = await client.purgeBucketCache('test_bucket');
161+
162+
final request = customHttpClient.receivedRequests.last;
163+
expect(request.method, 'DELETE');
164+
expect(
165+
request.url.toString(),
166+
endsWith('/storage/v1/cdn/test_bucket'),
167+
);
168+
expect(request.url.query, isEmpty);
169+
expect(response, 'success');
170+
});
171+
172+
test('should purgeBucketCache with transformations query param', () async {
173+
customHttpClient.response = {'message': 'success'};
174+
175+
final response = await client.purgeBucketCache(
176+
'test_bucket',
177+
transformations: true,
178+
);
179+
180+
final request = customHttpClient.receivedRequests.last;
181+
expect(request.method, 'DELETE');
182+
expect(request.url.queryParameters['transformations'], 'true');
183+
expect(response, 'success');
184+
});
185+
157186
test('should create analytics bucket', () async {
158187
customHttpClient.response = testAnalyticsBucketJson;
159188

@@ -248,6 +277,37 @@ void main() {
248277
expect(response, 'Move');
249278
});
250279

280+
test('should purgeCache issuing DELETE to /cdn/{bucket}/{path}', () async {
281+
customHttpClient.response = {'message': 'success'};
282+
283+
final response = await client.from('public').purgeCache('folder/a.txt');
284+
285+
final request = customHttpClient.receivedRequests.last;
286+
expect(request.method, 'DELETE');
287+
expect(
288+
request.url.toString(),
289+
endsWith('/storage/v1/cdn/public/folder/a.txt'),
290+
);
291+
expect(request.url.query, isEmpty);
292+
expect(response, 'success');
293+
});
294+
295+
test('should purgeCache with transformations query param', () async {
296+
customHttpClient.response = {'message': 'success'};
297+
298+
final response = await client
299+
.from('public')
300+
.purgeCache('folder/a.txt', transformations: true);
301+
302+
final request = customHttpClient.receivedRequests.last;
303+
expect(request.method, 'DELETE');
304+
expect(
305+
request.url.queryParameters['transformations'],
306+
'true',
307+
);
308+
expect(response, 'success');
309+
});
310+
251311
test('should createSignedUrl file', () async {
252312
customHttpClient.response = {'signedURL': '/signed/url'};
253313

sdk-compliance.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,14 @@ features:
438438
storage.file_buckets.copy: implemented
439439
storage.file_buckets.copy_cross_bucket: implemented
440440
storage.file_buckets.remove: implemented
441+
storage.file_buckets.purge_cache:
442+
status: implemented
443+
symbols:
444+
- StorageFileApi.purgeCache
445+
storage.file_buckets.purge_bucket_cache:
446+
status: implemented
447+
symbols:
448+
- StorageBucketApi.purgeBucketCache
441449
storage.file_buckets.create_signed_url:
442450
status: implemented
443451
symbols:

0 commit comments

Comments
 (0)