Skip to content

Commit 4d3f4bf

Browse files
authored
feat(storage_client): Custom-metadata, exists, info methods (#1023)
1 parent ce582e3 commit 4d3f4bf

File tree

6 files changed

+245
-74
lines changed

6 files changed

+245
-74
lines changed
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
FROM supabase/storage-api:v1.8.2
1+
FROM supabase/storage-api:v1.18.1
22

33
RUN apk add curl --no-cache

packages/storage_client/lib/src/fetch.dart

+73-32
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,19 @@ class Fetch {
2727
return MediaType.parse(mime ?? 'application/octet-stream');
2828
}
2929

30-
StorageException _handleError(dynamic error, StackTrace stack, Uri? url) {
30+
StorageException _handleError(
31+
dynamic error,
32+
StackTrace stack,
33+
Uri? url,
34+
FetchOptions? options,
35+
) {
3136
if (error is http.Response) {
37+
if (options?.noResolveJson == true) {
38+
return StorageException(
39+
error.body.isEmpty ? error.reasonPhrase ?? '' : error.body,
40+
statusCode: '${error.statusCode}',
41+
);
42+
}
3243
try {
3344
final data = json.decode(error.body) as Map<String, dynamic>;
3445

@@ -79,7 +90,7 @@ class Fetch {
7990
return _handleResponse(streamedResponse, options);
8091
}
8192

82-
Future<dynamic> _handleMultipartRequest(
93+
Future<dynamic> _handleFileRequest(
8394
String method,
8495
String url,
8596
File file,
@@ -88,7 +99,6 @@ class Fetch {
8899
int retryAttempts,
89100
StorageRetryController? retryController,
90101
) async {
91-
final headers = options?.headers ?? {};
92102
final contentType = fileOptions.contentType != null
93103
? MediaType.parse(fileOptions.contentType!)
94104
: _parseMediaType(file.path);
@@ -98,31 +108,15 @@ class Fetch {
98108
filename: file.path,
99109
contentType: contentType,
100110
);
101-
final request = http.MultipartRequest(method, Uri.parse(url))
102-
..headers.addAll(headers)
103-
..files.add(multipartFile)
104-
..fields['cacheControl'] = fileOptions.cacheControl
105-
..headers['x-upsert'] = fileOptions.upsert.toString();
106-
107-
final http.StreamedResponse streamedResponse;
108-
final r = RetryOptions(maxAttempts: (retryAttempts + 1));
109-
var attempts = 0;
110-
streamedResponse = await r.retry<http.StreamedResponse>(
111-
() async {
112-
attempts++;
113-
_log.finest('Request: attempt: $attempts $method $url $headers');
114-
if (httpClient != null) {
115-
return httpClient!.send(request);
116-
} else {
117-
return request.send();
118-
}
119-
},
120-
retryIf: (error) =>
121-
retryController?.cancelled != true &&
122-
(error is ClientException || error is TimeoutException),
111+
return _handleMultipartRequest(
112+
method,
113+
url,
114+
multipartFile,
115+
fileOptions,
116+
options,
117+
retryAttempts,
118+
retryController,
123119
);
124-
125-
return _handleResponse(streamedResponse, options);
126120
}
127121

128122
Future<dynamic> _handleBinaryFileRequest(
@@ -134,7 +128,6 @@ class Fetch {
134128
int retryAttempts,
135129
StorageRetryController? retryController,
136130
) async {
137-
final headers = options?.headers ?? {};
138131
final contentType = fileOptions.contentType != null
139132
? MediaType.parse(fileOptions.contentType!)
140133
: _parseMediaType(url);
@@ -145,11 +138,38 @@ class Fetch {
145138
filename: '',
146139
contentType: contentType,
147140
);
141+
return _handleMultipartRequest(
142+
method,
143+
url,
144+
multipartFile,
145+
fileOptions,
146+
options,
147+
retryAttempts,
148+
retryController,
149+
);
150+
}
151+
152+
Future<dynamic> _handleMultipartRequest(
153+
String method,
154+
String url,
155+
MultipartFile multipartFile,
156+
FileOptions fileOptions,
157+
FetchOptions? options,
158+
int retryAttempts,
159+
StorageRetryController? retryController,
160+
) async {
161+
final headers = options?.headers ?? {};
148162
final request = http.MultipartRequest(method, Uri.parse(url))
149163
..headers.addAll(headers)
150164
..files.add(multipartFile)
151165
..fields['cacheControl'] = fileOptions.cacheControl
152166
..headers['x-upsert'] = fileOptions.upsert.toString();
167+
if (fileOptions.metadata != null) {
168+
request.fields['metadata'] = json.encode(fileOptions.metadata);
169+
}
170+
if (fileOptions.headers != null) {
171+
request.headers.addAll(fileOptions.headers!);
172+
}
153173

154174
final http.StreamedResponse streamedResponse;
155175
final r = RetryOptions(maxAttempts: (retryAttempts + 1));
@@ -185,10 +205,24 @@ class Fetch {
185205
return jsonBody;
186206
}
187207
} else {
188-
throw _handleError(response, StackTrace.current, response.request?.url);
208+
throw _handleError(
209+
response,
210+
StackTrace.current,
211+
response.request?.url,
212+
options,
213+
);
189214
}
190215
}
191216

217+
Future<dynamic> head(String url, {FetchOptions? options}) async {
218+
return _handleRequest(
219+
'HEAD',
220+
url,
221+
null,
222+
FetchOptions(headers: options?.headers, noResolveJson: true),
223+
);
224+
}
225+
192226
Future<dynamic> get(String url, {FetchOptions? options}) async {
193227
return _handleRequest('GET', url, null, options);
194228
}
@@ -225,8 +259,15 @@ class Fetch {
225259
required int retryAttempts,
226260
required StorageRetryController? retryController,
227261
}) async {
228-
return _handleMultipartRequest('POST', url, file, fileOptions, options,
229-
retryAttempts, retryController);
262+
return _handleFileRequest(
263+
'POST',
264+
url,
265+
file,
266+
fileOptions,
267+
options,
268+
retryAttempts,
269+
retryController,
270+
);
230271
}
231272

232273
Future<dynamic> putFile(
@@ -237,7 +278,7 @@ class Fetch {
237278
required int retryAttempts,
238279
required StorageRetryController? retryController,
239280
}) async {
240-
return _handleMultipartRequest(
281+
return _handleFileRequest(
241282
'PUT',
242283
url,
243284
file,

packages/storage_client/lib/src/storage_file_api.dart

+30
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,36 @@ class StorageFileApi {
411411
return response as Uint8List;
412412
}
413413

414+
/// Retrieves the details of an existing file
415+
Future<FileObjectV2> info(String path) async {
416+
final finalPath = _getFinalPath(path);
417+
final options = FetchOptions(headers: headers);
418+
final response = await _storageFetch.get(
419+
'$url/object/info/$finalPath',
420+
options: options,
421+
);
422+
final fileObjects = FileObjectV2.fromJson(response);
423+
return fileObjects;
424+
}
425+
426+
/// Checks the existence of a file
427+
Future<bool> exists(String path) async {
428+
final finalPath = _getFinalPath(path);
429+
final options = FetchOptions(headers: headers);
430+
try {
431+
await _storageFetch.head(
432+
'$url/object/$finalPath',
433+
options: options,
434+
);
435+
return true;
436+
} on StorageException catch (e) {
437+
if (e.statusCode == '400' || e.statusCode == '404') {
438+
return false;
439+
}
440+
rethrow;
441+
}
442+
}
443+
414444
/// Retrieve URLs for assets in public buckets
415445
///
416446
/// [path] is the file path to be downloaded, including the current file name.

packages/storage_client/lib/src/types.dart

+57
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,53 @@ class FileObject {
7575
json['buckets'] != null ? Bucket.fromJson(json['buckets']) : null;
7676
}
7777

78+
class FileObjectV2 {
79+
final String id;
80+
final String version;
81+
final String name;
82+
final String bucketId;
83+
final String? updatedAt;
84+
final String createdAt;
85+
final String? lastAccessedAt;
86+
final int? size;
87+
final String? cacheControl;
88+
final String? contentType;
89+
final String? etag;
90+
final String? lastModified;
91+
final Map<String, dynamic>? metadata;
92+
93+
const FileObjectV2({
94+
required this.id,
95+
required this.version,
96+
required this.name,
97+
required this.bucketId,
98+
required this.updatedAt,
99+
required this.createdAt,
100+
required this.lastAccessedAt,
101+
required this.size,
102+
required this.cacheControl,
103+
required this.contentType,
104+
required this.etag,
105+
required this.lastModified,
106+
required this.metadata,
107+
});
108+
109+
FileObjectV2.fromJson(Map<String, dynamic> json)
110+
: id = json['id'] as String,
111+
version = json['version'] as String,
112+
name = json['name'] as String,
113+
bucketId = json['bucket_id'] as String,
114+
updatedAt = json['updated_at'] as String?,
115+
createdAt = json['created_at'] as String,
116+
lastAccessedAt = json['last_accessed_at'] as String?,
117+
size = json['size'] as int?,
118+
cacheControl = json['cache_control'] as String?,
119+
contentType = json['content_type'] as String?,
120+
etag = json['etag'] as String?,
121+
lastModified = json['last_modified'] as String?,
122+
metadata = json['metadata'] as Map<String, dynamic>?;
123+
}
124+
78125
/// [public] The visibility of the bucket. Public buckets don't require an
79126
/// authorization token to download objects, but still require a valid token for
80127
/// all other operations. By default, buckets are private.
@@ -115,10 +162,20 @@ class FileOptions {
115162
/// Throws a FormatError if the media type is invalid.
116163
final String? contentType;
117164

165+
/// The metadata option is an object that allows you to store additional
166+
/// information about the file. This information can be used to filter and
167+
/// search for files.
168+
final Map<String, dynamic>? metadata;
169+
170+
/// Optionally add extra headers.
171+
final Map<String, String>? headers;
172+
118173
const FileOptions({
119174
this.cacheControl = '3600',
120175
this.upsert = false,
121176
this.contentType,
177+
this.metadata,
178+
this.headers,
122179
});
123180
}
124181

packages/storage_client/test/basic_test.dart

+5-6
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@ String get objectUrl => '$supabaseUrl/storage/v1/object';
3535
void main() {
3636
late SupabaseStorageClient client;
3737
late CustomHttpClient customHttpClient = CustomHttpClient();
38+
tearDown(() {
39+
final file = File('a.txt');
40+
if (file.existsSync()) file.deleteSync();
41+
});
3842

39-
group('Client with default http client', () {
43+
group('Client with custom http client', () {
4044
setUp(() {
4145
// init SupabaseClient with test url & test key
4246
client = SupabaseStorageClient(
@@ -48,11 +52,6 @@ void main() {
4852
);
4953
});
5054

51-
tearDown(() {
52-
final file = File('a.txt');
53-
if (file.existsSync()) file.deleteSync();
54-
});
55-
5655
test('should list buckets', () async {
5756
customHttpClient.response = [testBucketJson, testBucketJson];
5857

0 commit comments

Comments
 (0)