Skip to content

Commit b0795f3

Browse files
committed
feat: info, exists and upload with metadat
1 parent 2e44d79 commit b0795f3

File tree

5 files changed

+164
-30
lines changed

5 files changed

+164
-30
lines changed

packages/storage_client/lib/src/fetch.dart

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ class Fetch {
2525
return MediaType.parse(mime ?? 'application/octet-stream');
2626
}
2727

28-
StorageException _handleError(dynamic error, StackTrace stack) {
29-
if (error is http.Response) {
28+
StorageException _handleError(
29+
dynamic error,
30+
StackTrace stack,
31+
FetchOptions? options,
32+
) {
33+
if (error is http.Response && !(options?.noResolveJson == true)) {
3034
try {
3135
final data = json.decode(error.body) as Map<String, dynamic>;
3236
return StorageException.fromJson(data, '${error.statusCode}');
@@ -70,7 +74,7 @@ class Fetch {
7074
return _handleResponse(streamedResponse, options);
7175
}
7276

73-
Future<dynamic> _handleMultipartRequest(
77+
Future<dynamic> _handleFileRequest(
7478
String method,
7579
String url,
7680
File file,
@@ -79,7 +83,6 @@ class Fetch {
7983
int retryAttempts,
8084
StorageRetryController? retryController,
8185
) async {
82-
final headers = options?.headers ?? {};
8386
final contentType = fileOptions.contentType != null
8487
? MediaType.parse(fileOptions.contentType!)
8588
: _parseMediaType(file.path);
@@ -89,28 +92,15 @@ class Fetch {
8992
filename: file.path,
9093
contentType: contentType,
9194
);
92-
final request = http.MultipartRequest(method, Uri.parse(url))
93-
..headers.addAll(headers)
94-
..files.add(multipartFile)
95-
..fields['cacheControl'] = fileOptions.cacheControl
96-
..headers['x-upsert'] = fileOptions.upsert.toString();
97-
98-
final http.StreamedResponse streamedResponse;
99-
final r = RetryOptions(maxAttempts: (retryAttempts + 1));
100-
streamedResponse = await r.retry<http.StreamedResponse>(
101-
() async {
102-
if (httpClient != null) {
103-
return httpClient!.send(request);
104-
} else {
105-
return request.send();
106-
}
107-
},
108-
retryIf: (error) =>
109-
retryController?.cancelled != true &&
110-
(error is ClientException || error is TimeoutException),
95+
return _handleMultipartRequest(
96+
method,
97+
url,
98+
multipartFile,
99+
fileOptions,
100+
options,
101+
retryAttempts,
102+
retryController,
111103
);
112-
113-
return _handleResponse(streamedResponse, options);
114104
}
115105

116106
Future<dynamic> _handleBinaryFileRequest(
@@ -122,7 +112,6 @@ class Fetch {
122112
int retryAttempts,
123113
StorageRetryController? retryController,
124114
) async {
125-
final headers = options?.headers ?? {};
126115
final contentType = fileOptions.contentType != null
127116
? MediaType.parse(fileOptions.contentType!)
128117
: _parseMediaType(url);
@@ -133,11 +122,38 @@ class Fetch {
133122
filename: '',
134123
contentType: contentType,
135124
);
125+
return _handleMultipartRequest(
126+
method,
127+
url,
128+
multipartFile,
129+
fileOptions,
130+
options,
131+
retryAttempts,
132+
retryController,
133+
);
134+
}
135+
136+
Future<dynamic> _handleMultipartRequest(
137+
String method,
138+
String url,
139+
MultipartFile multipartFile,
140+
FileOptions fileOptions,
141+
FetchOptions? options,
142+
int retryAttempts,
143+
StorageRetryController? retryController,
144+
) async {
145+
final headers = options?.headers ?? {};
136146
final request = http.MultipartRequest(method, Uri.parse(url))
137147
..headers.addAll(headers)
138148
..files.add(multipartFile)
139149
..fields['cacheControl'] = fileOptions.cacheControl
140150
..headers['x-upsert'] = fileOptions.upsert.toString();
151+
if (fileOptions.metadata != null) {
152+
request.fields['metadata'] = json.encode(fileOptions.metadata);
153+
}
154+
if (fileOptions.headers != null) {
155+
request.headers.addAll(fileOptions.headers!);
156+
}
141157

142158
final http.StreamedResponse streamedResponse;
143159
final r = RetryOptions(maxAttempts: (retryAttempts + 1));
@@ -170,10 +186,19 @@ class Fetch {
170186
return jsonBody;
171187
}
172188
} else {
173-
throw _handleError(response, StackTrace.current);
189+
throw _handleError(response, StackTrace.current, options);
174190
}
175191
}
176192

193+
Future<dynamic> head(String url, {FetchOptions? options}) async {
194+
return _handleRequest(
195+
'HEAD',
196+
url,
197+
null,
198+
FetchOptions(headers: options?.headers, noResolveJson: true),
199+
);
200+
}
201+
177202
Future<dynamic> get(String url, {FetchOptions? options}) async {
178203
return _handleRequest('GET', url, null, options);
179204
}
@@ -210,7 +235,7 @@ class Fetch {
210235
required int retryAttempts,
211236
required StorageRetryController? retryController,
212237
}) async {
213-
return _handleMultipartRequest('POST', url, file, fileOptions, options,
238+
return _handleFileRequest('POST', url, file, fileOptions, options,
214239
retryAttempts, retryController);
215240
}
216241

@@ -222,7 +247,7 @@ class Fetch {
222247
required int retryAttempts,
223248
required StorageRetryController? retryController,
224249
}) async {
225-
return _handleMultipartRequest(
250+
return _handleFileRequest(
226251
'PUT',
227252
url,
228253
file,

packages/storage_client/lib/src/storage_file_api.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,29 @@ class StorageFileApi {
397397
return response as Uint8List;
398398
}
399399

400+
/// Retrieves the details of an existing file
401+
Future<FileObjectV2> info(String path) async {
402+
final finalPath = _getFinalPath(path);
403+
final options = FetchOptions(headers: headers);
404+
final response = await _storageFetch.get(
405+
'$url/object/info/$finalPath',
406+
options: options,
407+
);
408+
final fileObjects = FileObjectV2.fromJson(response);
409+
return fileObjects;
410+
}
411+
412+
/// Checks the existence of a file
413+
Future<bool> exists(String path) async {
414+
final finalPath = _getFinalPath(path);
415+
final options = FetchOptions(headers: headers);
416+
final response = await _storageFetch.head(
417+
'$url/object/$finalPath',
418+
options: options,
419+
);
420+
return true;
421+
}
422+
400423
/// Retrieve URLs for assets in public buckets
401424
///
402425
/// [path] is the file path to be downloaded, including the current file name.
@@ -458,6 +481,7 @@ class StorageFileApi {
458481
body,
459482
options: options,
460483
);
484+
print(response[0]);
461485
final fileObjects = List<FileObject>.from(
462486
(response as List).map(
463487
(item) => FileObject.fromJson(item),

packages/storage_client/lib/src/types.dart

Lines changed: 57 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ void main() {
3636
late SupabaseStorageClient client;
3737
late CustomHttpClient customHttpClient = CustomHttpClient();
3838

39-
group('Client with default http client', () {
39+
group('Client with custom http client', () {
4040
setUp(() {
4141
// init SupabaseClient with test url & test key
4242
client = SupabaseStorageClient(

packages/storage_client/test/client_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,32 @@ void main() {
389389
await storage.from(newBucketName).copy(uploadPath, "$uploadPath 2");
390390
});
391391
});
392+
393+
test('upload with custom metadata', () async {
394+
final metadata = {
395+
'custom': 'metadata',
396+
'second': 'second',
397+
'third': 'third',
398+
};
399+
final path = "$uploadPath-metadata";
400+
await storage.from(newBucketName).upload(
401+
path,
402+
file,
403+
fileOptions: FileOptions(
404+
metadata: metadata,
405+
),
406+
);
407+
408+
final updateRes = await storage.from(newBucketName).info(path);
409+
expect(updateRes.metadata, metadata);
410+
});
411+
412+
test('check if object exists', () async {
413+
await storage.from(newBucketName).upload(uploadPath, file);
414+
final res = await storage.from(newBucketName).exists(uploadPath);
415+
expect(res, true);
416+
417+
final res2 = await storage.from(newBucketName).exists('not-exist');
418+
expect(res2, false);
419+
});
392420
}

0 commit comments

Comments
 (0)