diff --git a/lib/minio.dart b/lib/minio.dart index 087f4a5435cd5d8f241b6f51e05aa3a05cbc7944..113198ef31fe5e9a2a581dcb2ec1d5b10ac4f3a7 100644 --- a/lib/minio.dart +++ b/lib/minio.dart @@ -1,8 +1,6 @@ -/// Support for doing something awesome. -/// -/// More dartdocs go here. library minio; export 'src/minio.dart'; +export 'src/minio_errors.dart'; // TODO: Export any libraries intended for clients of this package. diff --git a/lib/src/minio.dart b/lib/src/minio.dart index 1545b58f1de6b0d27d9bab13be548b164800205a..670ddb1a80aedff180714172c79a6b3090a7c435 100644 --- a/lib/src/minio.dart +++ b/lib/src/minio.dart @@ -67,16 +67,34 @@ class Minio { 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 { - await _client.request(method: 'HEAD', bucket: bucket); + 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') return false; + 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; } - return true; } int _calculatePartSize(int size) { @@ -441,6 +459,7 @@ class Minio { 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)).toList(); diff --git a/lib/src/minio_helpers.dart b/lib/src/minio_helpers.dart index b8d2e381b4993c16fd4936aef317a1b9fd49aa47..4cd2fdc13e97ece4aabecf0e65aacda27eae296d 100644 --- a/lib/src/minio_helpers.dart +++ b/lib/src/minio_helpers.dart @@ -217,9 +217,17 @@ Future<void> validateStreamed( void validate(Response response, {int expect}) { if (response.statusCode >= 400) { - final body = xml.XmlDocument.parse(response.body); - final error = Error.fromXml(body.rootElement); - throw MinioS3Error(error.message, error, response); + var error; + + // Parse HTTP response body as XML only when not empty + if (response.body == null || response.body.isEmpty) { + error = Error(response.reasonPhrase, null, response.reasonPhrase, null); + } else { + final body = xml.XmlDocument.parse(response.body); + error = Error.fromXml(body.rootElement); + } + + throw MinioS3Error(error?.message, error, response); } if (expect != null && response.statusCode != expect) { diff --git a/test/minio_dart_test.dart b/test/minio_dart_test.dart index f6775d5abcf6a772254b007b6870b59be68f6721..e8626bcee58a4aaa0effa6be67cd70482564e658 100644 --- a/test/minio_dart_test.dart +++ b/test/minio_dart_test.dart @@ -1,14 +1,119 @@ +import 'package:minio/minio.dart'; +import 'package:test/test.dart'; void main() { - // group('A group of tests', () { - // Awesome awesome; + group('listBuckets', () { + test('listBuckets() succeeds', () async { + final minio = _getClient(); - // setUp(() { - // awesome = Awesome(); - // }); + expect(() async => await minio.listBuckets(), returnsNormally); + }); - // test('First Test', () { - // expect(awesome.isAwesome, isTrue); - // }); - // }); + test('listBuckets() fails due to wrong access key', () async { + final minio = _getClient(accessKey: 'incorrect-access-key'); + + expect( + () async => await minio.listBuckets(), + throwsA( + isA<MinioError>().having( + (e) => e.message, + 'message', + 'The Access Key Id you provided does not exist in our records.', + ), + ), + ); + }); + + test('listBuckets() fails due to wrong secret key', () async { + final minio = _getClient(secretKey: 'incorrect-secret-key'); + + expect( + () async => await minio.listBuckets(), + throwsA( + isA<MinioError>().having( + (e) => e.message, + 'message', + 'The request signature we calculated does not match the signature you provided. Check your key and signing method.', + ), + ), + ); + }); + }); + + group('bucketExists', () { + final bucketName = DateTime.now().millisecondsSinceEpoch.toString(); + + setUpAll(() async { + final minio = _getClient(); + await minio.makeBucket(bucketName); + }); + + tearDownAll(() async { + final minio = _getClient(); + await minio.removeBucket(bucketName); + }); + + test('bucketExists() returns true for an existing bucket', () async { + final minio = _getClient(); + expect(await minio.bucketExists(bucketName), equals(true)); + }); + + test('bucketExists() returns false for a non-existent bucket', () async { + final minio = _getClient(); + expect(await minio.bucketExists('non-existing-bucket-name'), equals(false)); + }); + + test('bucketExists() fails due to wrong access key', () async { + final minio = _getClient(accessKey: 'incorrect-access-key'); + expect( + () async => await minio.bucketExists(bucketName), + throwsA( + isA<MinioError>().having( + (e) => e.message, + 'message', + 'Forbidden', + ), + ), + ); + }); + + test('bucketExists() fails due to wrong secret key', () async { + final minio = _getClient(secretKey: 'incorrect-secret-key'); + expect( + () async => await minio.bucketExists(bucketName), + throwsA( + isA<MinioError>().having( + (e) => e.message, + 'message', + 'Forbidden', + ), + ), + ); + }); + }); } + +/// Initializes an instance of [Minio] with per default valid configuration. +/// +/// Don't worry, these credentials for MinIO are publicly available and +/// connect only to the MinIO demo server at `play.minio.io`. +Minio _getClient({ + String endpoint = 'play.minio.io', + int port = 443, + bool useSSL = true, + String accessKey = 'Q3AM3UQ867SPQQA43P2F', + String secretKey = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG', + String sessionToken = '', + String region = 'us-east-1', + bool enableTrace = false, +}) => + Minio( + endPoint: endpoint, + port: port, + useSSL: useSSL, + accessKey: accessKey, + secretKey: secretKey, + sessionToken: sessionToken, + region: region, + enableTrace: enableTrace, + );