import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:minio/models.dart'; import 'package:minio/src/minio_client.dart'; import 'package:minio/src/minio_errors.dart'; import 'package:minio/src/minio_helpers.dart'; import 'package:minio/src/minio_poller.dart'; import 'package:minio/src/minio_sign.dart'; import 'package:minio/src/minio_stream.dart'; import 'package:minio/src/minio_uploader.dart'; import 'package:minio/src/utils.dart'; import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' show XmlElement; import '../models.dart'; import 'minio_helpers.dart'; class Minio { /// Initializes a new client object. Minio({ required this.endPoint, required this.accessKey, required this.secretKey, int? port, this.useSSL = true, this.sessionToken, this.region, this.enableTrace = false, }) : port = port ?? implyPort(useSSL) { if (!isValidEndpoint(endPoint)) { throw MinioInvalidEndpointError( 'End point $endPoint is not a valid domain or ip address', ); } if (!isValidPort(this.port)) { throw MinioInvalidPortError( 'Invalid port number ${this.port}', ); } _client = MinioClient(this); } /// default part size for multipart uploads. final partSize = 64 * 1024 * 1024; /// maximum part size for multipart uploads. final maximumPartSize = 5 * 1024 * 1024 * 1024; /// maximum object size (5TB) final maxObjectSize = 5 * 1024 * 1024 * 1024 * 1024; /// endPoint is a host name or an IP address. /// /// For example: /// - play.min.io /// - 1.2.3.4 final String endPoint; /// TCP/IP port number. This input is optional. Default value set to 80 for HTTP and 443 for HTTPs. final int port; /// If set to true, https is used instead of http. Default is true. final bool useSSL; /// accessKey is like user-id that uniquely identifies your account. final String accessKey; /// secretKey is the password to your account. final String secretKey; /// Set this value to provide x-amz-security-token (AWS S3 specific). (Optional) final String? sessionToken; /// Set this value to override region cache. (Optional) final String? region; /// Set this value to enable tracing. (Optional) final bool enableTrace; late MinioClient _client; final _regionMap = <String?, String>{}; /// Checks if a bucket exists. /// /// Returns `true` only if the [bucket] exists and you have the permission /// to access it. Returns `false` if the [bucket] does not exist or you /// don't have the permission to access it. Future<bool> bucketExists(String bucket) async { MinioInvalidBucketNameError.check(bucket); try { final response = await _client.request(method: 'HEAD', bucket: bucket); validate(response); return response.statusCode == 200; } on MinioS3Error catch (e) { final code = e.error?.code; if (code == 'NoSuchBucket' || code == 'NotFound' || code == 'Not Found') { return false; } rethrow; } on StateError catch (e) { // Insight from testing: in most cases, AWS S3 returns the HTTP status code // 404 when a bucket does not exist. Whereas in other cases, when the bucket // does not exist, S3 returns the HTTP status code 301 Redirect instead of // status code 404 as officially documented. Then, this redirect response // lacks the HTTP header `location` which causes this exception in Dart's // HTTP library (`http_impl.dart`). if (e.message == 'Response has no Location header for redirect') { return false; } rethrow; } } int _calculatePartSize(int size) { assert(size >= 0); if (size > maxObjectSize) { throw ArgumentError('size should not be more than $maxObjectSize'); } var partSize = this.partSize; while (true) { if ((partSize * 10000) > size) { return partSize; } partSize += 16 * 1024 * 1024; } } /// Complete the multipart upload. After all the parts are uploaded issuing /// this call will aggregate the parts on the server into a single object. Future<String> completeMultipartUpload( String bucket, String object, String uploadId, List<CompletedPart> parts, ) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); var queries = {'uploadId': uploadId}; var payload = CompleteMultipartUpload(parts).toXml().toString(); final resp = await _client.request( method: 'POST', bucket: bucket, object: object, queries: queries, payload: payload, ); validate(resp, expect: 200); final node = xml.XmlDocument.parse(resp.body); final errorNode = node.findAllElements('Error'); if (errorNode.isNotEmpty) { final error = Error.fromXml(errorNode.first); throw MinioS3Error(error.message, error, resp); } final etag = node.findAllElements('ETag').first.text; return etag; } /// Copy the object. Future<CopyObjectResult> copyObject( String bucket, String object, String srcObject, [ CopyConditions? conditions, ]) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); MinioInvalidObjectNameError.check(srcObject); final headers = <String, String>{}; headers['x-amz-copy-source'] = srcObject; if (conditions != null) { if (conditions.modified != null) { headers['x-amz-copy-source-if-modified-since'] = conditions.modified!; } if (conditions.unmodified != null) { headers['x-amz-copy-source-if-unmodified-since'] = conditions.unmodified!; } if (conditions.matchETag != null) { headers['x-amz-copy-source-if-match'] = conditions.matchETag!; } if (conditions.matchETagExcept != null) { headers['x-amz-copy-source-if-none-match'] = conditions.matchETagExcept!; } } final resp = await _client.request( method: 'PUT', bucket: bucket, object: object, headers: headers, ); validate(resp); final node = xml.XmlDocument.parse(resp.body); final result = CopyObjectResult.fromXml(node.rootElement); result.eTag = trimDoubleQuote(result.eTag!); return result; } /// Find uploadId of an incomplete upload. Future<String?> findUploadId(String bucket, String object) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); MultipartUpload? latestUpload; String? keyMarker; String? uploadIdMarker; bool? isTruncated = false; do { final result = await listIncompleteUploadsQuery( bucket, object, keyMarker, uploadIdMarker, '', ); for (var upload in result.uploads) { if (upload.key != object) continue; if (latestUpload == null || upload.initiated!.isAfter(latestUpload.initiated!)) { latestUpload = upload; } } keyMarker = result.nextKeyMarker; uploadIdMarker = result.nextUploadIdMarker; isTruncated = result.isTruncated; } while (isTruncated!); return latestUpload?.uploadId; } /// Return the list of notification configurations stored /// in the S3 provider Future<NotificationConfiguration> getBucketNotification(String bucket) async { MinioInvalidBucketNameError.check(bucket); final resp = await _client.request( method: 'GET', bucket: bucket, resource: 'notification', ); validate(resp, expect: 200); final node = xml.XmlDocument.parse(resp.body); return NotificationConfiguration.fromXml(node.rootElement); } /// Get the bucket policy associated with the specified bucket. If `objectPrefix` /// is not empty, the bucket policy will be filtered based on object permissions /// as well. Future<Map<String, dynamic>?> getBucketPolicy(bucket) async { MinioInvalidBucketNameError.check(bucket); final resp = await _client.request( method: 'GET', bucket: bucket, resource: 'policy', ); validate(resp, expect: 200); return json.decode(resp.body); } /// Gets the region of [bucket]. The region is cached for subsequent calls. Future<String> getBucketRegion(String bucket) async { MinioInvalidBucketNameError.check(bucket); if (region != null) { return region!; } if (_regionMap.containsKey(bucket)) { return _regionMap[bucket]!; } final resp = await _client.request( method: 'GET', bucket: bucket, region: 'us-east-1', queries: <String, dynamic>{'location': null}, ); validate(resp); final node = xml.XmlDocument.parse(resp.body); var location = node.findAllElements('LocationConstraint').first.text; // if (location == null || location.isEmpty) { if (location.isEmpty) { location = 'us-east-1'; } _regionMap[bucket] = location; return location; } /// get a readable stream of the object content. Future<MinioByteStream> getObject(String bucket, String object) { return getPartialObject(bucket, object, null, null); } /// get a readable stream of the partial object content. Future<MinioByteStream> getPartialObject( String bucket, String object, [ int? offset, int? length, ]) async { assert(offset == null || offset >= 0); assert(length == null || length > 0); MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); String? range; if (offset != null || length != null) { if (offset != null) { range = 'bytes=$offset-'; } else { range = 'bytes=0-'; offset = 0; } if (length != null) { range += '${(length + offset) - 1}'; } } final headers = range != null ? {'range': range} : null; final expectedStatus = range != null ? 206 : 200; final resp = await _client.requestStream( method: 'GET', bucket: bucket, object: object, headers: headers, ); await validateStreamed(resp, expect: expectedStatus); return MinioByteStream.fromStream( stream: resp.stream, contentLength: resp.contentLength, ); } /// Initiate a new multipart upload. Future<String> initiateNewMultipartUpload( String bucket, String object, Map<String, String>? metaData, ) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); final resp = await _client.request( method: 'POST', bucket: bucket, object: object, headers: metaData, resource: 'uploads', ); validate(resp, expect: 200); final node = xml.XmlDocument.parse(resp.body); return node.findAllElements('UploadId').first.text; } /// Returns a stream that emits objects that are partially uploaded. Stream<IncompleteUpload> listIncompleteUploads( String bucket, String prefix, [ bool recursive = false, ]) async* { MinioInvalidBucketNameError.check(bucket); MinioInvalidPrefixError.check(prefix); final delimiter = recursive ? '' : '/'; String? keyMarker; String? uploadIdMarker; var isTruncated = false; do { final result = await listIncompleteUploadsQuery( bucket, prefix, keyMarker, uploadIdMarker, delimiter, ); for (var upload in result.uploads) { final parts = listParts(bucket, upload.key!, upload.uploadId!); final size = await parts.fold(0, (dynamic acc, item) => acc + item.size); yield IncompleteUpload(upload: upload, size: size); } keyMarker = result.nextKeyMarker; uploadIdMarker = result.nextUploadIdMarker; isTruncated = result.isTruncated!; } while (isTruncated); } /// Called by listIncompleteUploads to fetch a batch of incomplete uploads. Future<ListMultipartUploadsOutput> listIncompleteUploadsQuery( String bucket, String prefix, String? keyMarker, String? uploadIdMarker, String delimiter, ) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidPrefixError.check(prefix); var queries = <String, dynamic>{ 'uploads': null, 'prefix': prefix, 'delimiter': delimiter, }; if (keyMarker != null) { queries['key-marker'] = keyMarker; } if (uploadIdMarker != null) { queries['upload-id-marker'] = uploadIdMarker; } final resp = await _client.request( method: 'GET', bucket: bucket, resource: 'uploads', queries: queries, ); validate(resp); final node = xml.XmlDocument.parse(resp.body); return ListMultipartUploadsOutput.fromXml(node.root as XmlElement); } /// Listen for notifications on a bucket. Additionally one can provider /// filters for prefix, suffix and events. There is no prior set bucket notification /// needed to use this API. **This is an MinIO extension API** where unique identifiers /// are regitered and unregistered by the server automatically based on incoming requests. NotificationPoller listenBucketNotification( String bucket, { String? prefix, String? suffix, List<String>? events, }) { MinioInvalidBucketNameError.check(bucket); final poller = NotificationPoller(_client, bucket, prefix, suffix, events); poller.start(); return poller; } /// List of buckets created. Future<List<Bucket>> listBuckets() async { final resp = await _client.request( method: 'GET', region: region ?? 'us-east-1', ); validate(resp); final bucketsNode = xml.XmlDocument.parse(resp.body).findAllElements('Buckets').first; return bucketsNode.children .map((n) => Bucket.fromXml(n as XmlElement)) .toList(); } /// Returns all [Object]s in a bucket. /// To list objects in a bucket with prefix, set [prefix] to the desired prefix. Stream<ListObjectsResult> listObjects( String bucket, { String prefix = '', bool recursive = false, }) async* { MinioInvalidBucketNameError.check(bucket); MinioInvalidPrefixError.check(prefix); final delimiter = recursive ? '' : '/'; String? marker; var isTruncated = false; do { final resp = await listObjectsQuery( bucket, prefix, marker, delimiter, 1000, ); isTruncated = resp.isTruncated!; marker = resp.nextMarker; yield ListObjectsResult( objects: resp.contents!, prefixes: resp.commonPrefixes.map((e) => e.prefix!).toList(), ); } while (isTruncated); } /// Returns all [Object]s in a bucket. This is a shortcut for [listObjects]. /// Use [listObjects] to list buckets with a large number of objects. Future<ListObjectsResult> listAllObjects( String bucket, { String prefix = '', bool recursive = false, }) async { final chunks = listObjects(bucket, prefix: prefix, recursive: recursive); final objects = <Object>[]; final prefixes = <String>[]; await for (final chunk in chunks) { objects.addAll(chunk.objects); prefixes.addAll(chunk.prefixes); } return ListObjectsResult( objects: objects, prefixes: prefixes, ); } /// list a batch of objects Future<ListObjectsOutput> listObjectsQuery( String bucket, String prefix, String? marker, String delimiter, int? maxKeys, ) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidPrefixError.check(prefix); final queries = <String, dynamic>{}; queries['prefix'] = prefix; queries['delimiter'] = delimiter; if (marker != null) { queries['marker'] = marker; } if (maxKeys != null) { maxKeys = maxKeys >= 1000 ? 1000 : maxKeys; queries['maxKeys'] = maxKeys.toString(); } final resp = await _client.request( method: 'GET', bucket: bucket, queries: queries, ); validate(resp); final node = xml.XmlDocument.parse(resp.body); final isTruncated = getNodeProp(node.rootElement, 'IsTruncated')!.text; final nextMarker = getNodeProp(node.rootElement, 'NextMarker')?.text; final objs = node.findAllElements('Contents').map((c) => Object.fromXml(c)); final prefixes = node .findAllElements('CommonPrefixes') .map((c) => CommonPrefix.fromXml(c)); return ListObjectsOutput() ..contents = objs.toList() ..commonPrefixes = prefixes.toList() ..isTruncated = isTruncated.toLowerCase() == 'true' ..nextMarker = nextMarker; } /// Returns all [Object]s in a bucket. /// To list objects in a bucket with prefix, set [prefix] to the desired prefix. /// This uses ListObjectsV2 in the S3 API. For backward compatibility, use /// [listObjects] instead. Stream<ListObjectsResult> listObjectsV2( String bucket, { String prefix = '', bool recursive = false, String? startAfter, }) async* { MinioInvalidBucketNameError.check(bucket); MinioInvalidPrefixError.check(prefix); final delimiter = recursive ? '' : '/'; bool? isTruncated = false; String? continuationToken; do { final resp = await listObjectsV2Query( bucket, prefix, continuationToken, delimiter, 1000, startAfter, ); isTruncated = resp.isTruncated; continuationToken = resp.nextContinuationToken; yield ListObjectsResult( objects: resp.contents!, prefixes: resp.commonPrefixes.map((e) => e.prefix!).toList(), ); } while (isTruncated!); } /// Returns all [Object]s in a bucket. This is a shortcut for [listObjectsV2]. /// Use [listObjects] to list buckets with a large number of objects. /// This uses ListObjectsV2 in the S3 API. For backward compatibility, use /// [listAllObjects] instead. Future<ListObjectsResult> listAllObjectsV2( String bucket, { String prefix = '', bool recursive = false, }) async { final chunks = listObjects(bucket, prefix: prefix, recursive: recursive); final objects = <Object>[]; final prefixes = <String>[]; await for (final chunk in chunks) { objects.addAll(chunk.objects); prefixes.addAll(chunk.prefixes); } return ListObjectsResult( objects: objects, prefixes: prefixes, ); } /// listObjectsV2Query - (List Objects V2) - List some or all (up to 1000) of the objects in a bucket. Future<ListObjectsV2Output> listObjectsV2Query( String bucket, String prefix, String? continuationToken, String delimiter, int? maxKeys, String? startAfter, ) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidPrefixError.check(prefix); final queries = <String, dynamic>{}; queries['prefix'] = prefix; queries['delimiter'] = delimiter; queries['list-type'] = '2'; if (continuationToken != null) { queries['continuation-token'] = continuationToken; } if (startAfter != null) { queries['start-after'] = startAfter; } if (maxKeys != null) { maxKeys = maxKeys >= 1000 ? 1000 : maxKeys; queries['maxKeys'] = maxKeys.toString(); } final resp = await _client.request( method: 'GET', bucket: bucket, queries: queries, ); validate(resp); final node = xml.XmlDocument.parse(resp.body); final isTruncated = getNodeProp(node.rootElement, 'IsTruncated')!.text; final nextContinuationToken = getNodeProp(node.rootElement, 'NextContinuationToken')?.text; final objs = node.findAllElements('Contents').map((c) => Object.fromXml(c)); final prefixes = node .findAllElements('CommonPrefixes') .map((c) => CommonPrefix.fromXml(c)); return ListObjectsV2Output() ..contents = objs.toList() ..commonPrefixes = prefixes.toList() ..isTruncated = isTruncated.toLowerCase() == 'true' ..nextContinuationToken = nextContinuationToken; } /// Get part-info of all parts of an incomplete upload specified by uploadId. Stream<Part> listParts( String bucket, String object, String uploadId, ) async* { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); var marker = 0; var isTruncated = false; do { final result = await listPartsQuery(bucket, object, uploadId, marker); marker = result.nextPartNumberMarker; isTruncated = result.isTruncated; yield* Stream.fromIterable(result.parts); } while (isTruncated); } /// Called by listParts to fetch a batch of part-info Future<ListPartsOutput> listPartsQuery( String? bucket, String? object, String? uploadId, int? marker, ) async { var queries = <String, dynamic>{'uploadId': uploadId}; if (marker != null && marker != 0) { queries['part-number-marker'] = marker.toString(); } final resp = await _client.request( method: 'GET', bucket: bucket, object: object, queries: queries, ); validate(resp); final node = xml.XmlDocument.parse(resp.body); return ListPartsOutput.fromXml(node.root as XmlElement); } /// Creates the bucket [bucket]. Future<void> makeBucket(String bucket, [String? region]) async { MinioInvalidBucketNameError.check(bucket); if (this.region != null && region != null && this.region != region) { throw MinioInvalidArgumentError( 'Configured region ${this.region}, requested $region', ); } region ??= this.region ?? 'us-east-1'; final payload = region == 'us-east-1' ? '' : CreateBucketConfiguration(region).toXml().toString(); final resp = await _client.request( method: 'PUT', bucket: bucket, region: region, payload: payload, ); validate(resp); // return resp.body; } /// Generate a presigned URL for GET /// /// - [bucketName]: name of the bucket /// - [objectName]: name of the object /// - [expires]: expiry in seconds (optional, default 7 days) /// - [respHeaders]: response headers to override (optional) /// - [requestDate]: A date object, the url will be issued at (optional) Future<String> presignedGetObject( String bucket, String object, { int? expires, Map<String, String>? respHeaders, DateTime? requestDate, }) { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); return presignedUrl( 'GET', bucket, object, expires: expires, reqParams: respHeaders, requestDate: requestDate, ); } /// presignedPostPolicy can be used in situations where we want more control on the upload than what /// presignedPutObject() provides. i.e Using presignedPostPolicy we will be able to put policy restrictions /// on the object's `name` `bucket` `expiry` `Content-Type` Future presignedPostPolicy(PostPolicy postPolicy) async { if (_client.anonymous) { throw MinioAnonymousRequestError( 'Presigned POST policy cannot be generated for anonymous requests', ); } final region = await getBucketRegion(postPolicy.formData['bucket']!); var date = DateTime.now().toUtc(); var dateStr = makeDateLong(date); if (postPolicy.policy['expiration'] == null) { // 'expiration' is mandatory field for S3. // Set default expiration date of 7 days. var expires = DateTime.now().toUtc(); expires.add(Duration(days: 7)); postPolicy.setExpires(expires); } postPolicy.policy['conditions'].push(['eq', r'$x-amz-date', dateStr]); postPolicy.formData['x-amz-date'] = dateStr; postPolicy.policy['conditions'] .push(['eq', r'$x-amz-algorithm', 'AWS4-HMAC-SHA256']); postPolicy.formData['x-amz-algorithm'] = 'AWS4-HMAC-SHA256'; postPolicy.policy['conditions'].push( ['eq', r'$x-amz-credential', accessKey + '/' + getScope(region, date)]); postPolicy.formData['x-amz-credential'] = accessKey + '/' + getScope(region, date); if (sessionToken != null) { postPolicy.policy['conditions'] .push(['eq', r'$x-amz-security-token', sessionToken]); } final policyBase64 = jsonBase64(postPolicy.policy); postPolicy.formData['policy'] = policyBase64; final signature = postPresignSignatureV4(region, date, secretKey, policyBase64); postPolicy.formData['x-amz-signature'] = signature; final url = _client .getBaseRequest('POST', postPolicy.formData['bucket'], null, region, null, null, null, null) .url; var portStr = (port == 80 || port == 443) ? '' : ':$port'; var urlStr = '${url.scheme}://${url.host}$portStr${url.path}'; return PostPolicyResult(postURL: urlStr, formData: postPolicy.formData); } /// Generate a presigned URL for PUT. /// Using this URL, the browser can upload to S3 only with the specified object name. /// /// - [bucketName]: name of the bucket /// - [objectName]: name of the object /// - [expires]: expiry in seconds (optional, default 7 days) Future<String> presignedPutObject( String bucket, String object, { int? expires, }) { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); return presignedUrl('PUT', bucket, object, expires: expires); } /// Generate a generic presigned URL which can be /// used for HTTP methods GET, PUT, HEAD and DELETE /// /// - [method]: name of the HTTP method /// - [bucketName]: name of the bucket /// - [objectName]: name of the object /// - [expires]: expiry in seconds (optional, default 7 days) /// - [reqParams]: request parameters (optional) /// - [requestDate]: A date object, the url will be issued at (optional) Future<String> presignedUrl( String method, String bucket, String object, { int? expires, String? resource, Map<String, String>? reqParams, DateTime? requestDate, }) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); if (expires != null && expires < 0) { throw MinioInvalidArgumentError('invalid expire time value: $expires'); } expires ??= expires = 24 * 60 * 60 * 7; // 7 days in seconds reqParams ??= {}; requestDate ??= DateTime.now().toUtc(); final region = await getBucketRegion(bucket); final request = _client.getBaseRequest( method, bucket, object, region, resource, reqParams, {}, null, ); return presignSignatureV4(this, request, region, requestDate, expires); } /// Uploads the object. Returns the ETag of the uploaded object. Future<String> putObject( String bucket, String object, Stream<Uint8List> data, { int? size, int? chunkSize, Map<String, String>? metadata, void Function(int)? onProgress, }) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); if (size != null && size < 0) { throw MinioInvalidArgumentError('invalid size value: $size'); } if (chunkSize != null && chunkSize < 5 * 1024 * 1024) { throw MinioInvalidArgumentError('Minimum chunk size is 5MB'); } metadata = prependXAMZMeta(metadata ?? <String, String>{}); final partSize = chunkSize ?? _calculatePartSize(size ?? maxObjectSize); final uploader = MinioUploader( this, _client, bucket, object, partSize, metadata, onProgress, ); final chunker = BlockStream(partSize); final etag = await data.transform(chunker).pipe(uploader); return etag.toString(); } /// Remove all bucket notification Future<void> removeAllBucketNotification(String bucket) async { await setBucketNotification( bucket, NotificationConfiguration(null, null, null), ); } /// Remove a bucket. Future<void> removeBucket(String bucket) async { MinioInvalidBucketNameError.check(bucket); final resp = await _client.request( method: 'DELETE', bucket: bucket, ); validate(resp, expect: 204); _regionMap.remove(bucket); } /// Remove the partially uploaded object. Future<void> removeIncompleteUpload(String bucket, String object) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); final uploadId = await findUploadId(bucket, object); if (uploadId == null) return; final resp = await _client.request( method: 'DELETE', bucket: bucket, object: object, queries: {'uploadId': uploadId}, ); validate(resp, expect: 204); } /// Remove the specified object. Future<void> removeObject(String bucket, String object) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); final resp = await _client.request( method: 'DELETE', bucket: bucket, object: object, ); validate(resp, expect: 204); } /// Remove all the objects residing in the objectsList. Future<void> removeObjects(String bucket, List<String> objects) async { MinioInvalidBucketNameError.check(bucket); final bunches = groupList(objects, 1000); for (var bunch in bunches) { final payload = Delete( bunch.map((key) => ObjectIdentifier(key, null)).toList(), true, ).toXml().toString(); final headers = {'Content-MD5': md5Base64(payload)}; await _client.request( method: 'POST', bucket: bucket, resource: 'delete', headers: headers, payload: payload, ); } } // Remove all the notification configurations in the S3 provider Future<void> setBucketNotification( String bucket, NotificationConfiguration config, ) async { MinioInvalidBucketNameError.check(bucket); final resp = await _client.request( method: 'PUT', bucket: bucket, resource: 'notification', payload: config.toXml().toString(), ); validate(resp, expect: 200); } /// Set the bucket policy on the specified bucket. /// /// [policy] is detailed [here](https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html). Future<void> setBucketPolicy( String bucket, [ Map<String, dynamic>? policy, ]) async { MinioInvalidBucketNameError.check(bucket); final method = policy != null ? 'PUT' : 'DELETE'; final payload = policy != null ? json.encode(policy) : ''; final resp = await _client.request( method: method, bucket: bucket, resource: 'policy', payload: payload, ); validate(resp, expect: 204); } Future<void> setObjectACL(String bucket, String object, String policy) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); await _client.request( method: 'PUT', bucket: bucket, object: object, queries: {'acl': policy}, ); } Future<AccessControlPolicy> getObjectACL(String bucket, String object) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); final resp = await _client.request( method: 'GET', bucket: bucket, object: object, queries: {'acl': ''}, ); return AccessControlPolicy.fromXml( xml.XmlDocument.parse(resp.body) .findElements('AccessControlPolicy') .first, ); } /// Stat information of the object. Future<StatObjectResult> statObject(String bucket, String object) async { MinioInvalidBucketNameError.check(bucket); MinioInvalidObjectNameError.check(object); final resp = await _client.request( method: 'HEAD', bucket: bucket, object: object, ); validate(resp, expect: 200); var etag = resp.headers['etag']; if (etag != null) { etag = trimDoubleQuote(etag); } return StatObjectResult( etag: etag, size: int.parse(resp.headers['content-length']!), metaData: extractMetadata(resp.headers), lastModified: parseRfc7231Time(resp.headers['last-modified']!), acl: await getObjectACL(bucket, object), ); } }