├── example
├── custed.png
├── filebase_example.dart
├── miniox_example.dart
└── minio_example.dart
├── lib
├── models.dart
├── minio.dart
├── src
│ ├── minio_stream.dart
│ ├── minio_s3.dart
│ ├── minio_poller.dart
│ ├── minio_errors.dart
│ ├── utils.dart
│ ├── minio_uploader.dart
│ ├── minio_models.dart
│ ├── minio_sign.dart
│ ├── minio_helpers.dart
│ ├── minio_client.dart
│ └── minio.dart
└── io.dart
├── analysis_options.yaml
├── .github
├── workflows
│ ├── test.yml
│ └── lint.yml
└── dependabot.yml
├── .gitignore
├── test
├── minio_models_test.dart
├── minio_stream_test.dart
├── minio_presigned_url_test.dart
├── helpers.dart
├── minio_helpers_test.dart
├── utils_test.dart
└── minio_test.dart
├── pubspec.yaml
├── LICENSE
├── CHANGELOG.md
├── util
└── generate_models.dart
└── README.md
/example/custed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xtyxtyx/minio-dart/HEAD/example/custed.png
--------------------------------------------------------------------------------
/lib/models.dart:
--------------------------------------------------------------------------------
1 | export 'src/minio_models.dart';
2 | export 'src/minio_models_generated.dart';
3 |
--------------------------------------------------------------------------------
/lib/minio.dart:
--------------------------------------------------------------------------------
1 | library minio;
2 |
3 | export 'src/minio.dart';
4 | export 'src/minio_errors.dart';
5 | export 'src/minio_stream.dart';
6 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:mostly_reasonable_lints/analysis_options.yaml
2 | analyzer:
3 | errors:
4 | avoid_print: ignore
5 | prefer_const_declarations: ignore
6 |
--------------------------------------------------------------------------------
/lib/src/minio_stream.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | class MinioByteStream extends StreamView> {
4 | MinioByteStream.fromStream({
5 | required Stream> stream,
6 | required this.contentLength,
7 | }) : super(stream);
8 |
9 | final int? contentLength;
10 | }
11 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: dart-lang/setup-dart@v1
15 |
16 | - run: dart pub get
17 | - run: dart test
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Files and directories created by pub
2 | .dart_tool/
3 | .packages
4 | # Remove the following pattern if you wish to check in your lock file
5 | pubspec.lock
6 |
7 | # Conventional directory for build outputs
8 | build/
9 |
10 | # Directory created by dartdoc
11 | doc/api/
12 |
13 | # IDE directories
14 | .idea/
15 |
16 | # For debugging purpose
17 | test/main.dart
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: weekly
7 | groups:
8 | github-actions:
9 | patterns:
10 | - "*"
11 | - package-ecosystem: pub
12 | directory: /
13 | schedule:
14 | interval: weekly
15 | groups:
16 | root-pub:
17 | patterns:
18 | - "*"
19 |
--------------------------------------------------------------------------------
/example/filebase_example.dart:
--------------------------------------------------------------------------------
1 | import 'package:minio/minio.dart';
2 |
3 | void main() async {
4 | final minio = Minio(
5 | endPoint: 's3.filebase.com',
6 | accessKey: '',
7 | secretKey: '',
8 | useSSL: true,
9 | );
10 |
11 | final buckets = await minio.listBuckets();
12 | print('buckets: $buckets');
13 |
14 | final objects = await minio.listObjects(buckets.first.name).toList();
15 | print('objects: $objects');
16 | }
17 |
--------------------------------------------------------------------------------
/example/miniox_example.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:minio/io.dart';
4 | import 'package:minio/minio.dart';
5 |
6 | void main() async {
7 | final minio = Minio(
8 | endPoint: 'play.min.io',
9 | accessKey: 'Q3AM3UQ867SPQQA43P2F',
10 | secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
11 | );
12 |
13 | await minio.fPutObject('testbucket', 'test.png', 'example/custed.png');
14 |
15 | final stat = await minio.statObject('testbucket', 'test.png');
16 | assert(stat.size == File('example/custed.png').lengthSync());
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | analyze:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: dart-lang/setup-dart@v1
15 |
16 | - run: dart pub get
17 | - run: dart analyze
18 |
19 | format:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v4
23 | - uses: dart-lang/setup-dart@v1
24 |
25 | - run: dart pub get
26 | - run: dart format --output=none --set-exit-if-changed .
27 |
--------------------------------------------------------------------------------
/test/minio_models_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:minio/models.dart';
2 | import 'package:test/test.dart';
3 |
4 | void main() {
5 | final date = DateTime.utc(2017, 8, 11, 19, 34, 18);
6 | final dateString = 'Fri, 11 Aug 2017 19:34:18 GMT';
7 |
8 | test('CopyConditions.setModified() works', () {
9 | final cc = CopyConditions();
10 | cc.setModified(date);
11 | expect(cc.modified, equals(dateString));
12 | });
13 |
14 | test('CopyConditions.setUnmodified() works', () {
15 | final cc = CopyConditions();
16 | cc.setUnmodified(date);
17 | expect(cc.unmodified, dateString);
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: minio
2 | description: Unofficial MinIO Dart Client SDK that provides simple APIs to access any Amazon S3 compatible object storage server.
3 | version: 3.5.8
4 | homepage: https://github.com/xtyxtyx/minio-dart
5 | issue_tracker: https://github.com/xtyxtyx/minio-dart/issues
6 |
7 | environment:
8 | sdk: ">=3.0.0 <4.0.0"
9 |
10 | dependencies:
11 | buffer: ^1.2.3
12 | convert: ^3.1.1
13 | crypto: ^3.0.3
14 | http: ^1.1.0
15 | intl: ">=0.19.0 <0.21.0"
16 | meta: ^1.12.0
17 | mime: ">=1.0.4 <3.0.0"
18 | path: ^1.8.0
19 | xml: ^6.4.2
20 |
21 | dev_dependencies:
22 | html: ^0.15.4
23 | mostly_reasonable_lints: ^0.1.2
24 | test: ^1.25.5
25 |
--------------------------------------------------------------------------------
/lib/src/minio_s3.dart:
--------------------------------------------------------------------------------
1 | const awsS3Endpoint = {
2 | 'us-east-1': 's3.amazonaws.com',
3 | 'us-east-2': 's3-us-east-2.amazonaws.com',
4 | 'us-west-1': 's3-us-west-1.amazonaws.com',
5 | 'us-west-2': 's3-us-west-2.amazonaws.com',
6 | 'ca-central-1': 's3.ca-central-1.amazonaws.com',
7 | 'eu-west-1': 's3-eu-west-1.amazonaws.com',
8 | 'eu-west-2': 's3-eu-west-2.amazonaws.com',
9 | 'sa-east-1': 's3-sa-east-1.amazonaws.com',
10 | 'eu-central-1': 's3-eu-central-1.amazonaws.com',
11 | 'ap-south-1': 's3-ap-south-1.amazonaws.com',
12 | 'ap-southeast-1': 's3-ap-southeast-1.amazonaws.com',
13 | 'ap-southeast-2': 's3-ap-southeast-2.amazonaws.com',
14 | 'ap-northeast-1': 's3-ap-northeast-1.amazonaws.com',
15 | 'cn-north-1': 's3.cn-north-1.amazonaws.com.cn',
16 | };
17 |
18 | // getS3Endpoint get relevant endpoint for the region.
19 | String getS3Endpoint(String region) {
20 | final endpoint = awsS3Endpoint[region];
21 | return endpoint ?? 's3.amazonaws.com';
22 | }
23 |
--------------------------------------------------------------------------------
/test/minio_stream_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:test/test.dart';
4 |
5 | import 'helpers.dart';
6 |
7 | void main() {
8 | group('MinioByteStream', () {
9 | final bucketName = uniqueName();
10 | final objectName = 'content-length-test';
11 | final testData = Uint8List.fromList([1, 2, 3, 4, 5]);
12 |
13 | setUpAll(() async {
14 | final minio = getMinioClient();
15 | await minio.makeBucket(bucketName);
16 | await minio.putObject(bucketName, objectName, Stream.value(testData));
17 | });
18 |
19 | tearDownAll(() async {
20 | final minio = getMinioClient();
21 | await minio.removeObject(bucketName, objectName);
22 | await minio.removeBucket(bucketName);
23 | });
24 |
25 | test('contains content length', () async {
26 | final minio = getMinioClient();
27 | final stream = await minio.getObject(bucketName, objectName);
28 | expect(stream.contentLength, equals(testData.length));
29 | await stream.drain();
30 | });
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/test/minio_presigned_url_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:minio/minio.dart';
2 | import 'package:test/test.dart';
3 |
4 | import 'helpers.dart';
5 |
6 | void main() {
7 | test('Minio.presignedGetObject() works', () async {
8 | final minio = getMinioClient();
9 | await minio.presignedGetObject('bucket', 'object');
10 | });
11 |
12 | test('Minio.presignedGetObject() throws when [expires] < 0', () async {
13 | final minio = getMinioClient();
14 | expect(
15 | () => minio.presignedGetObject('bucket', 'object', expires: -1),
16 | throwsA(isA()),
17 | );
18 | });
19 |
20 | test('Minio.presignedPutObject() works', () async {
21 | final minio = getMinioClient();
22 | await minio.presignedPutObject('bucket', 'object');
23 | });
24 |
25 | test('Minio.presignedPutObject() throws when [expires] < 0', () async {
26 | final minio = getMinioClient();
27 | expect(
28 | () => minio.presignedPutObject('bucket', 'object', expires: -1),
29 | throwsA(isA()),
30 | );
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 xuty
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/helpers.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:minio/minio.dart';
4 |
5 | /// Initializes an instance of [Minio] with per default valid configuration.
6 | ///
7 | /// Don't worry, these credentials for MinIO are publicly available and
8 | /// connect only to the MinIO demo server at `play.minio.io`.
9 | Minio getMinioClient({
10 | String endpoint = 'play.minio.io',
11 | int? port = 443,
12 | bool useSSL = true,
13 | String accessKey = 'Q3AM3UQ867SPQQA43P2F',
14 | String secretKey = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
15 | String sessionToken = '',
16 | String region = 'us-east-1',
17 | bool enableTrace = false,
18 | }) =>
19 | Minio(
20 | endPoint: endpoint,
21 | port: port,
22 | useSSL: useSSL,
23 | accessKey: accessKey,
24 | secretKey: secretKey,
25 | sessionToken: sessionToken,
26 | region: region,
27 | enableTrace: enableTrace,
28 | );
29 |
30 | /// Generates a random name for a bucket or object.
31 | String uniqueName() {
32 | final random = Random();
33 | final now = DateTime.now();
34 | final name = 'id-${now.microsecondsSinceEpoch}-${random.nextInt(8192)}';
35 | return name;
36 | }
37 |
--------------------------------------------------------------------------------
/test/minio_helpers_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:minio/src/minio_helpers.dart';
2 | import 'package:test/test.dart';
3 |
4 | void main() {
5 | group('helpers', () {
6 | test('should validate for s3 endpoint', () {
7 | expect(isValidEndpoint('s3.amazonaws.com'), isTrue);
8 | });
9 | test('should validate for s3 china', () {
10 | expect(isValidEndpoint('s3.cn-north-1.amazonaws.com.cn'), isTrue);
11 | });
12 | test('should validate for us-west-2', () {
13 | expect(isValidEndpoint('s3-us-west-2.amazonaws.com'), isTrue);
14 | });
15 | test('should fail for invalid endpoint characters', () {
16 | expect(isValidEndpoint('111.#2.11'), isFalse);
17 | });
18 | test('should validate for valid ip', () {
19 | expect(isValidIPv4('1.1.1.1'), isTrue);
20 | });
21 | test('should fail for invalid ip', () {
22 | expect(isValidIPv4('1.1.1'), isFalse);
23 | });
24 | test('should make date short', () {
25 | final date = DateTime.parse('2012-12-03T17:25:36.331Z');
26 | expect(makeDateShort(date), '20121203');
27 | });
28 | test('should make date long', () {
29 | final date = DateTime.parse('2017-08-11T17:26:34.935Z');
30 | expect(makeDateLong(date), '20170811T172634Z');
31 | });
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/lib/src/minio_poller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 |
4 | import 'package:minio/src/minio_client.dart';
5 |
6 | class NotificationPoller {
7 | NotificationPoller(
8 | this._client,
9 | this.bucket,
10 | this.prefix,
11 | this.suffix,
12 | this.events,
13 | );
14 |
15 | final MinioClient _client;
16 | final String bucket;
17 | final String? prefix;
18 | final String? suffix;
19 | final List? events;
20 |
21 | final _eventStream = StreamController
4 |
5 | This is the _unofficial_ MinIO Dart Client SDK that provides simple APIs to access any Amazon S3 compatible object storage server.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## API
20 |
21 | | Bucket operations | Object operations | Presigned operations | Bucket Policy & Notification operations |
22 | | ----------------------- | ------------------------ | --------------------- | --------------------------------------- |
23 | | [makeBucket] | [getObject] | [presignedUrl] | [getBucketNotification] |
24 | | [listBuckets] | [getPartialObject] | [presignedGetObject] | [setBucketNotification] |
25 | | [bucketExists] | [fGetObject] | [presignedPutObject] | [removeAllBucketNotification] |
26 | | [removeBucket] | [putObject] | [presignedPostPolicy] | [listenBucketNotification] |
27 | | [listObjects] | [fPutObject] | | [getBucketPolicy] |
28 | | [listObjectsV2] | [copyObject] | | [setBucketPolicy] |
29 | | [listIncompleteUploads] | [statObject] | | |
30 | | [listAllObjects] | [removeObject] | | |
31 | | [listAllObjectsV2] | [removeObjects] | | |
32 | | | [removeIncompleteUpload] | | |
33 |
34 | ## Usage
35 |
36 | ### Initialize MinIO Client
37 |
38 | **MinIO**
39 |
40 | ```dart
41 | final minio = Minio(
42 | endPoint: 'play.min.io',
43 | accessKey: 'Q3AM3UQ867SPQQA43P2F',
44 | secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
45 | );
46 | ```
47 |
48 | **AWS S3**
49 |
50 | ```dart
51 | final minio = Minio(
52 | endPoint: 's3.amazonaws.com',
53 | accessKey: 'YOUR-ACCESSKEYID',
54 | secretKey: 'YOUR-SECRETACCESSKEY',
55 | );
56 | ```
57 |
58 | **Filebase**
59 |
60 | ```dart
61 | final minio = Minio(
62 | endPoint: 's3.filebase.com',
63 | accessKey: 'YOUR-ACCESSKEYID',
64 | secretKey: 'YOUR-SECRETACCESSKEY',
65 | useSSL: true,
66 | );
67 | ```
68 |
69 | **File upload**
70 |
71 | ```dart
72 | import 'package:minio/io.dart';
73 | import 'package:minio/minio.dart';
74 |
75 | void main() async {
76 | final minio = Minio(
77 | endPoint: 'play.min.io',
78 | accessKey: 'Q3AM3UQ867SPQQA43P2F',
79 | secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
80 | );
81 |
82 | await minio.fPutObject('mybucket', 'myobject', 'path/to/file');
83 | }
84 | ```
85 |
86 | For complete example, see: [example]
87 |
88 | > To use `fPutObject()` and `fGetObject`, you have to `import 'package:minio/io.dart';`
89 |
90 | **Upload with progress**
91 |
92 | ```dart
93 | import 'package:minio/minio.dart';
94 |
95 | void main() async {
96 | final minio = Minio(
97 | endPoint: 'play.min.io',
98 | accessKey: 'Q3AM3UQ867SPQQA43P2F',
99 | secretKey: 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
100 | );
101 |
102 | await minio.putObject(
103 | 'mybucket',
104 | 'myobject',
105 | Stream.value(Uint8List(1024)),
106 | onProgress: (bytes) => print('$bytes uploaded'),
107 | );
108 | }
109 | ```
110 |
111 | **Get object**
112 |
113 | ```dart
114 | import 'dart:io';
115 | import 'package:minio/minio.dart';
116 |
117 | void main() async {
118 | final minio = Minio(
119 | endPoint: 's3.amazonaws.com',
120 | accessKey: 'YOUR-ACCESSKEYID',
121 | secretKey: 'YOUR-SECRETACCESSKEY',
122 | );
123 |
124 | final stream = await minio.getObject('BUCKET-NAME', 'OBJECT-NAME');
125 |
126 | // Get object length
127 | print(stream.contentLength);
128 |
129 | // Write object data stream to file
130 | await stream.pipe(File('output.txt').openWrite());
131 | }
132 | ```
133 |
134 | ## Features and bugs
135 |
136 | Please file feature requests and bugs at the [issue tracker][tracker].
137 |
138 | Contributions to this repository are welcome.
139 |
140 | ## License
141 |
142 | [MIT](./LICENSE)
143 |
144 | [tracker]: https://github.com/xtyxtyx/minio-dart/issues
145 | [example]: https://pub.dev/packages/minio/example
146 |
147 | [makeBucket]: https://pub.dev/documentation/minio/latest/minio/Minio/makeBucket.html
148 | [listBuckets]: https://pub.dev/documentation/minio/latest/minio/Minio/listBuckets.html
149 | [bucketExists]: https://pub.dev/documentation/minio/latest/minio/Minio/bucketExists.html
150 | [removeBucket]: https://pub.dev/documentation/minio/latest/minio/Minio/removeBucket.html
151 | [listObjects]: https://pub.dev/documentation/minio/latest/minio/Minio/listObjects.html
152 | [listObjectsV2]: https://pub.dev/documentation/minio/latest/minio/Minio/listObjectsV2.html
153 | [listIncompleteUploads]: https://pub.dev/documentation/minio/latest/minio/Minio/listIncompleteUploads.html
154 | [listAllObjects]: https://pub.dev/documentation/minio/latest/minio/Minio/listAllObjects.html
155 | [listAllObjectsV2]: https://pub.dev/documentation/minio/latest/minio/Minio/listAllObjectsV2.html
156 |
157 | [getObject]: https://pub.dev/documentation/minio/latest/minio/Minio/getObject.html
158 | [getPartialObject]: https://pub.dev/documentation/minio/latest/minio/Minio/getPartialObject.html
159 | [putObject]: https://pub.dev/documentation/minio/latest/minio/Minio/putObject.html
160 | [copyObject]: https://pub.dev/documentation/minio/latest/minio/Minio/copyObject.html
161 | [statObject]: https://pub.dev/documentation/minio/latest/minio/Minio/statObject.html
162 | [removeObject]: https://pub.dev/documentation/minio/latest/minio/Minio/removeObject.html
163 | [removeObjects]: https://pub.dev/documentation/minio/latest/minio/Minio/removeObjects.html
164 | [removeIncompleteUpload]: https://pub.dev/documentation/minio/latest/minio/Minio/removeIncompleteUpload.html
165 |
166 | [fGetObject]: https://pub.dev/documentation/minio/latest/io/MinioX/fGetObject.html
167 | [fPutObject]: https://pub.dev/documentation/minio/latest/io/MinioX/fPutObject.html
168 |
169 | [presignedUrl]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedUrl.html
170 | [presignedGetObject]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedGetObject.html
171 | [presignedPutObject]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedPutObject.html
172 | [presignedPostPolicy]: https://pub.dev/documentation/minio/latest/minio/Minio/presignedPostPolicy.html
173 |
174 | [getBucketNotification]: https://pub.dev/documentation/minio/latest/minio/Minio/getBucketNotification.html
175 | [setBucketNotification]: https://pub.dev/documentation/minio/latest/minio/Minio/setBucketNotification.html
176 | [removeAllBucketNotification]: https://pub.dev/documentation/minio/latest/minio/Minio/removeAllBucketNotification.html
177 | [listenBucketNotification]: https://pub.dev/documentation/minio/latest/minio/Minio/listenBucketNotification.html
178 |
179 | [getBucketPolicy]: https://pub.dev/documentation/minio/latest/minio/Minio/getBucketPolicy.html
180 | [setBucketPolicy]: https://pub.dev/documentation/minio/latest/minio/Minio/setBucketPolicy.html
181 |
--------------------------------------------------------------------------------
/lib/src/minio_helpers.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: non_constant_identifier_names
2 |
3 | import 'package:convert/convert.dart';
4 | import 'package:http/http.dart';
5 | import 'package:mime/mime.dart' show lookupMimeType;
6 | import 'package:minio/src/minio_client.dart';
7 | import 'package:minio/src/minio_errors.dart';
8 | import 'package:minio/src/minio_models_generated.dart';
9 | import 'package:xml/xml.dart' as xml;
10 |
11 | bool isValidBucketName(String bucket) {
12 | if (bucket.length < 3 || bucket.length > 63) {
13 | return false;
14 | }
15 | if (bucket.contains('..')) {
16 | return false;
17 | }
18 |
19 | if (RegExp(r'[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+').hasMatch(bucket)) {
20 | return false;
21 | }
22 |
23 | if (RegExp(r'^[a-z0-9][a-z0-9.-]+[a-z0-9]$').hasMatch(bucket)) {
24 | return true;
25 | }
26 |
27 | return false;
28 | }
29 |
30 | bool isValidObjectName(String objectName) {
31 | if (!isValidPrefix(objectName)) return false;
32 | if (objectName.isEmpty) return false;
33 | return true;
34 | }
35 |
36 | bool isValidPrefix(String prefix) {
37 | if (prefix.length > 1024) return false;
38 | return true;
39 | }
40 |
41 | bool isAmazonEndpoint(String endpoint) {
42 | return endpoint == 's3.amazonaws.com' ||
43 | endpoint == 's3.cn-north-1.amazonaws.com.cn';
44 | }
45 |
46 | bool isVirtualHostStyle(String endpoint, bool useSSL, String? bucket) {
47 | if (bucket == null) {
48 | return false;
49 | }
50 |
51 | if (useSSL && bucket.contains('.')) {
52 | return false;
53 | }
54 |
55 | return isAmazonEndpoint(endpoint);
56 | }
57 |
58 | bool isValidEndpoint(endpoint) {
59 | return isValidDomain(endpoint) || isValidIPv4(endpoint);
60 | }
61 |
62 | bool isValidIPv4(String? ip) {
63 | if (ip == null) return false;
64 | return RegExp(r'^(\d{1,3}\.){3,3}\d{1,3}$').hasMatch(ip);
65 | }
66 |
67 | bool isValidDomain(String? host) {
68 | if (host == null) return false;
69 |
70 | if (host.isEmpty || host.length > 255) {
71 | return false;
72 | }
73 |
74 | if (host.startsWith('-') || host.endsWith('-')) {
75 | return false;
76 | }
77 |
78 | if (host.startsWith('_') || host.endsWith('_')) {
79 | return false;
80 | }
81 |
82 | if (host.startsWith('.') || host.endsWith('.')) {
83 | return false;
84 | }
85 |
86 | final alphaNumerics = '`~!@#\$%^&*()+={}[]|\\"\';:>/'.split('');
87 | for (var char in alphaNumerics) {
88 | if (host.contains(char)) return false;
89 | }
90 |
91 | return true;
92 | }
93 |
94 | bool isValidPort(int port) {
95 | if (port < 0) return false;
96 | if (port == 0) return true;
97 | const minPort = 1;
98 | const maxPort = 65535;
99 | return port >= minPort && port <= maxPort;
100 | }
101 |
102 | int implyPort(bool ssl) {
103 | return ssl ? 443 : 80;
104 | }
105 |
106 | String makeDateLong(DateTime date) {
107 | final isoDate = date.toIso8601String();
108 |
109 | // 'YYYYMMDDTHHmmss' + Z
110 | return '${isoDate.substring(0, 4)}'
111 | '${isoDate.substring(5, 7)}'
112 | '${isoDate.substring(8, 13)}'
113 | '${isoDate.substring(14, 16)}'
114 | '${isoDate.substring(17, 19)}Z';
115 | }
116 |
117 | String makeDateShort(DateTime date) {
118 | final isoDate = date.toIso8601String();
119 |
120 | // 'YYYYMMDD'
121 | return isoDate.substring(0, 4) +
122 | isoDate.substring(5, 7) +
123 | isoDate.substring(8, 10);
124 | }
125 |
126 | Map prependXAMZMeta(Map metadata) {
127 | final newMetadata = Map.from(metadata);
128 | for (var key in metadata.keys) {
129 | if (!isAmzHeader(key) &&
130 | !isSupportedHeader(key) &&
131 | !isStorageclassHeader(key)) {
132 | newMetadata['x-amz-meta-$key'] = newMetadata[key]!;
133 | newMetadata.remove(key);
134 | }
135 | }
136 | return newMetadata;
137 | }
138 |
139 | bool isAmzHeader(key) {
140 | key = key.toLowerCase();
141 | return key.startsWith('x-amz-meta-') ||
142 | key == 'x-amz-acl' ||
143 | key.startsWith('x-amz-server-side-encryption-') ||
144 | key == 'x-amz-server-side-encryption';
145 | }
146 |
147 | bool isSupportedHeader(key) {
148 | var supportedHeaders = {
149 | 'content-type',
150 | 'cache-control',
151 | 'content-encoding',
152 | 'content-disposition',
153 | 'content-language',
154 | 'x-amz-website-redirect-location',
155 | };
156 | return (supportedHeaders.contains(key.toLowerCase()));
157 | }
158 |
159 | bool isStorageclassHeader(key) {
160 | return key.toLowerCase() == 'x-amz-storage-class';
161 | }
162 |
163 | Map extractMetadata(Map metaData) {
164 | var newMetadata = {};
165 | for (var key in metaData.keys) {
166 | if (isSupportedHeader(key) ||
167 | isStorageclassHeader(key) ||
168 | isAmzHeader(key)) {
169 | if (key.toLowerCase().startsWith('x-amz-meta-')) {
170 | newMetadata[key.substring(11, key.length)] = metaData[key]!;
171 | } else {
172 | newMetadata[key] = metaData[key]!;
173 | }
174 | }
175 | }
176 | return newMetadata;
177 | }
178 |
179 | String probeContentType(String path) {
180 | final contentType = lookupMimeType(path);
181 | return contentType ?? 'application/octet-stream';
182 | }
183 |
184 | Map insertContentType(
185 | Map metaData,
186 | String filePath,
187 | ) {
188 | for (var key in metaData.keys) {
189 | if (key.toLowerCase() == 'content-type') {
190 | return metaData;
191 | }
192 | }
193 |
194 | final newMetadata = Map.from(metaData);
195 | newMetadata['content-type'] = probeContentType(filePath);
196 | return newMetadata;
197 | }
198 |
199 | Future validateStreamed(
200 | StreamedResponse streamedResponse, {
201 | int? expect,
202 | }) async {
203 | if (streamedResponse.statusCode >= 400) {
204 | final response = await MinioResponse.fromStream(streamedResponse);
205 | final body = xml.XmlDocument.parse(response.body);
206 | final error = Error.fromXml(body.rootElement);
207 | throw MinioS3Error(error.message, error, response);
208 | }
209 |
210 | if (expect != null && streamedResponse.statusCode != expect) {
211 | final response = await MinioResponse.fromStream(streamedResponse);
212 | throw MinioS3Error(
213 | '$expect expected, got ${streamedResponse.statusCode}',
214 | null,
215 | response,
216 | );
217 | }
218 | }
219 |
220 | void validate(MinioResponse response, {int? expect}) {
221 | if (response.statusCode >= 400) {
222 | dynamic error;
223 |
224 | // Parse HTTP response body as XML only when not empty
225 | if (response.body.isEmpty) {
226 | error = Error(response.reasonPhrase, null, response.reasonPhrase, null);
227 | } else {
228 | // check valid xml
229 | if (response.body.startsWith(' stream;
28 |
29 | if (body is Stream) {
30 | stream = body;
31 | } else if (body is String) {
32 | final data = const Utf8Encoder().convert(body);
33 | headers['content-length'] = data.length.toString();
34 | stream = Stream.value(data);
35 | } else if (body is Uint8List) {
36 | stream = Stream.value(body);
37 | headers['content-length'] = body.length.toString();
38 | } else {
39 | throw UnsupportedError('Unsupported body type: ${body.runtimeType}');
40 | }
41 |
42 | if (onProgress == null) {
43 | return ByteStream(stream);
44 | }
45 |
46 | var bytesRead = 0;
47 |
48 | stream = stream.transform(MaxChunkSize(1 << 16));
49 |
50 | return ByteStream(
51 | stream.transform(
52 | StreamTransformer.fromHandlers(
53 | handleData: (data, sink) {
54 | sink.add(data);
55 | bytesRead += data.length;
56 | onProgress!(bytesRead);
57 | },
58 | ),
59 | ),
60 | );
61 | }
62 |
63 | MinioRequest replace({
64 | String? method,
65 | Uri? url,
66 | Map? headers,
67 | body,
68 | }) {
69 | final result = MinioRequest(method ?? this.method, url ?? this.url);
70 | result.body = body ?? this.body;
71 | result.headers.addAll(headers ?? this.headers);
72 | return result;
73 | }
74 | }
75 |
76 | /// An HTTP response where the entire response body is known in advance.
77 | class MinioResponse extends BaseResponse {
78 | /// Create a new HTTP response with a byte array body.
79 | MinioResponse.bytes(
80 | this.bodyBytes,
81 | int statusCode, {
82 | BaseRequest? request,
83 | Map headers = const {},
84 | bool isRedirect = false,
85 | bool persistentConnection = true,
86 | String? reasonPhrase,
87 | }) : super(
88 | statusCode,
89 | contentLength: bodyBytes.length,
90 | request: request,
91 | headers: headers,
92 | isRedirect: isRedirect,
93 | persistentConnection: persistentConnection,
94 | reasonPhrase: reasonPhrase,
95 | );
96 |
97 | /// The bytes comprising the body of this response.
98 | final Uint8List bodyBytes;
99 |
100 | /// Body of s3 response is always encoded as UTF-8.
101 | String get body => utf8.decode(bodyBytes);
102 |
103 | static Future fromStream(StreamedResponse response) async {
104 | final body = await response.stream.toBytes();
105 | return MinioResponse.bytes(
106 | body,
107 | response.statusCode,
108 | request: response.request,
109 | headers: response.headers,
110 | isRedirect: response.isRedirect,
111 | persistentConnection: response.persistentConnection,
112 | reasonPhrase: response.reasonPhrase,
113 | );
114 | }
115 | }
116 |
117 | class MinioClient {
118 | MinioClient(this.minio) {
119 | anonymous = minio.accessKey.isEmpty && minio.secretKey.isEmpty;
120 | enableSHA256 = !anonymous && !minio.useSSL;
121 | port = minio.port;
122 | }
123 |
124 | final Minio minio;
125 | final String userAgent = 'MinIO (Unknown; Unknown) minio-dart/2.0.0';
126 |
127 | late bool enableSHA256;
128 | late bool anonymous;
129 | late final int port;
130 |
131 | Future _request({
132 | required String method,
133 | String? bucket,
134 | String? object,
135 | String? region,
136 | String? resource,
137 | dynamic payload = '',
138 | Map? queries,
139 | Map? headers,
140 | void Function(int)? onProgress,
141 | }) async {
142 | if (bucket != null) {
143 | region ??= await minio.getBucketRegion(bucket);
144 | }
145 |
146 | region ??= 'us-east-1';
147 |
148 | final request = getBaseRequest(
149 | method,
150 | bucket,
151 | object,
152 | region,
153 | resource,
154 | queries,
155 | headers,
156 | onProgress,
157 | );
158 | request.body = payload;
159 |
160 | final date = DateTime.now().toUtc();
161 | final sha256sum = enableSHA256 ? sha256Hex(payload) : 'UNSIGNED-PAYLOAD';
162 | request.headers.addAll({
163 | 'user-agent': userAgent,
164 | 'x-amz-date': makeDateLong(date),
165 | 'x-amz-content-sha256': sha256sum,
166 | });
167 |
168 | if (minio.sessionToken != null) {
169 | request.headers['x-amz-security-token'] = minio.sessionToken!;
170 | }
171 |
172 | final authorization = signV4(minio, request, date, region);
173 | request.headers['authorization'] = authorization;
174 | logRequest(request);
175 | final response = await request.send();
176 | return response;
177 | }
178 |
179 | Future request({
180 | required String method,
181 | String? bucket,
182 | String? object,
183 | String? region,
184 | String? resource,
185 | dynamic payload = '',
186 | Map? queries,
187 | Map? headers,
188 | void Function(int)? onProgress,
189 | }) async {
190 | final stream = await _request(
191 | method: method,
192 | bucket: bucket,
193 | object: object,
194 | region: region,
195 | payload: payload,
196 | resource: resource,
197 | queries: queries,
198 | headers: headers,
199 | onProgress: onProgress,
200 | );
201 |
202 | final response = await MinioResponse.fromStream(stream);
203 | logResponse(response);
204 |
205 | return response;
206 | }
207 |
208 | Future requestStream({
209 | required String method,
210 | String? bucket,
211 | String? object,
212 | String? region,
213 | String? resource,
214 | dynamic payload = '',
215 | Map? queries,
216 | Map? headers,
217 | }) async {
218 | final response = await _request(
219 | method: method,
220 | bucket: bucket,
221 | object: object,
222 | region: region,
223 | payload: payload,
224 | resource: resource,
225 | queries: queries,
226 | headers: headers,
227 | );
228 |
229 | logResponse(response);
230 | return response;
231 | }
232 |
233 | MinioRequest getBaseRequest(
234 | String method,
235 | String? bucket,
236 | String? object,
237 | String region,
238 | String? resource,
239 | Map? queries,
240 | Map? headers,
241 | void Function(int)? onProgress,
242 | ) {
243 | final url = getRequestUrl(bucket, object, resource, queries);
244 | final request = MinioRequest(method, url, onProgress: onProgress);
245 | request.headers['host'] = url.authority;
246 |
247 | if (headers != null) {
248 | request.headers.addAll(headers);
249 | }
250 |
251 | return request;
252 | }
253 |
254 | Uri getRequestUrl(
255 | String? bucket,
256 | String? object,
257 | String? resource,
258 | Map? queries,
259 | ) {
260 | var host = minio.endPoint.toLowerCase();
261 | var path = '/';
262 |
263 | bool pathStyle = minio.pathStyle ?? true;
264 | if (isAmazonEndpoint(host)) {
265 | host = getS3Endpoint(minio.region!);
266 | pathStyle = !isVirtualHostStyle(host, minio.useSSL, bucket);
267 | }
268 |
269 | if (!pathStyle) {
270 | if (bucket != null) host = '$bucket.$host';
271 | if (object != null) path = '/$object';
272 | } else {
273 | if (bucket != null) path = '/$bucket';
274 | if (object != null) path = '/$bucket/$object';
275 | }
276 |
277 | final query = StringBuffer();
278 | if (resource != null) {
279 | query.write(resource);
280 | }
281 | if (queries != null) {
282 | if (query.isNotEmpty) query.write('&');
283 | query.write(encodeQueries(queries));
284 | }
285 |
286 | return Uri(
287 | scheme: minio.useSSL ? 'https' : 'http',
288 | host: host,
289 | port: minio.port,
290 | pathSegments: path.split('/'),
291 | query: query.toString(),
292 | );
293 | }
294 |
295 | void logRequest(MinioRequest request) {
296 | if (!minio.enableTrace) return;
297 |
298 | final buffer = StringBuffer();
299 | buffer.writeln('REQUEST: ${request.method} ${request.url}');
300 | for (var header in request.headers.entries) {
301 | buffer.writeln('${header.key}: ${header.value}');
302 | }
303 |
304 | if (request.body is List) {
305 | buffer.writeln('List of size ${request.body.length}');
306 | } else {
307 | buffer.writeln(request.body);
308 | }
309 |
310 | print(buffer.toString());
311 | }
312 |
313 | void logResponse(BaseResponse response) {
314 | if (!minio.enableTrace) return;
315 |
316 | final buffer = StringBuffer();
317 | buffer.writeln('RESPONSE: ${response.statusCode} ${response.reasonPhrase}');
318 | for (var header in response.headers.entries) {
319 | buffer.writeln('${header.key}: ${header.value}');
320 | }
321 |
322 | if (response is Response) {
323 | buffer.writeln(response.body);
324 | } else if (response is StreamedResponse) {
325 | buffer.writeln('STREAMED BODY');
326 | }
327 |
328 | print(buffer.toString());
329 | }
330 | }
331 |
--------------------------------------------------------------------------------
/test/minio_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 | import 'dart:typed_data';
3 |
4 | import 'package:minio/io.dart';
5 | import 'package:minio/minio.dart';
6 | import 'package:minio/src/minio_models_generated.dart';
7 | import 'package:minio/src/utils.dart';
8 | import 'package:test/test.dart';
9 |
10 | import 'helpers.dart';
11 |
12 | void main() {
13 | testConstruct();
14 | testListBuckets();
15 | testBucketExists();
16 | testFPutObject();
17 | testGetObjectACL();
18 | testSetObjectACL();
19 | testGetObject();
20 | testPutObject();
21 | testGetBucketNotification();
22 | testSetBucketNotification();
23 | testRemoveAllBucketNotification();
24 | testListenBucketNotification();
25 | testStatObject();
26 | testMakeBucket();
27 | testRemoveBucket();
28 | testRemoveObject();
29 | testListObjects();
30 | }
31 |
32 | void testConstruct() {
33 | test('Minio() implies http port', () {
34 | final client = getMinioClient(port: null, useSSL: false);
35 | expect(client.port, equals(80));
36 | });
37 |
38 | test('Minio() implies https port', () {
39 | final client = getMinioClient(port: null, useSSL: true);
40 | expect(client.port, equals(443));
41 | });
42 |
43 | test('Minio() overrides port with http', () {
44 | final client = getMinioClient(port: 1234, useSSL: false);
45 | expect(client.port, equals(1234));
46 | });
47 |
48 | test('Minio() overrides port with https', () {
49 | final client = getMinioClient(port: 1234, useSSL: true);
50 | expect(client.port, equals(1234));
51 | });
52 |
53 | test('Minio() throws when endPoint is url', () {
54 | expect(
55 | () => getMinioClient(endpoint: 'http://play.min.io'),
56 | throwsA(isA()),
57 | );
58 | });
59 |
60 | test('Minio() throws when port is invalid', () {
61 | expect(
62 | () => getMinioClient(port: -1),
63 | throwsA(isA()),
64 | );
65 |
66 | expect(
67 | () => getMinioClient(port: 65536),
68 | throwsA(isA()),
69 | );
70 | });
71 | }
72 |
73 | void testListBuckets() {
74 | test('listBuckets() succeeds', () async {
75 | final minio = getMinioClient();
76 |
77 | expect(() async => await minio.listBuckets(), returnsNormally);
78 | });
79 |
80 | test('listBuckets() can list buckets', () async {
81 | final minio = getMinioClient();
82 | final bucketName1 = uniqueName();
83 | await minio.makeBucket(bucketName1);
84 |
85 | final bucketName2 = uniqueName();
86 | await minio.makeBucket(bucketName2);
87 |
88 | final buckets = await minio.listBuckets();
89 | expect(buckets.any((b) => b.name == bucketName1), isTrue);
90 | expect(buckets.any((b) => b.name == bucketName2), isTrue);
91 |
92 | await minio.removeBucket(bucketName1);
93 | await minio.removeBucket(bucketName2);
94 | });
95 |
96 | test('listBuckets() fails due to wrong access key', () async {
97 | final minio = getMinioClient(accessKey: 'incorrect-access-key');
98 |
99 | expect(
100 | () async => await minio.listBuckets(),
101 | throwsA(
102 | isA().having(
103 | (e) => e.error!.code,
104 | 'code',
105 | isIn(['AccessDenied', 'InvalidAccessKeyId']),
106 | ),
107 | ),
108 | );
109 | });
110 |
111 | test('listBuckets() fails due to wrong secret key', () async {
112 | final minio = getMinioClient(secretKey: 'incorrect-secret-key');
113 |
114 | expect(
115 | () async => await minio.listBuckets(),
116 | throwsA(
117 | isA().having(
118 | (e) => e.error!.code,
119 | 'code',
120 | isIn(['AccessDenied', 'SignatureDoesNotMatch']),
121 | ),
122 | ),
123 | );
124 | });
125 | }
126 |
127 | void testBucketExists() {
128 | group('bucketExists', () {
129 | final bucketName = uniqueName();
130 |
131 | setUpAll(() async {
132 | final minio = getMinioClient();
133 | await minio.makeBucket(bucketName);
134 | });
135 |
136 | tearDownAll(() async {
137 | final minio = getMinioClient();
138 | await minio.removeBucket(bucketName);
139 | });
140 |
141 | test('bucketExists() returns true for an existing bucket', () async {
142 | final minio = getMinioClient();
143 | expect(await minio.bucketExists(bucketName), equals(true));
144 | });
145 |
146 | test('bucketExists() returns false for a non-existent bucket', () async {
147 | final minio = getMinioClient();
148 | expect(
149 | await minio.bucketExists('non-existing-bucket-name'),
150 | equals(false),
151 | );
152 | });
153 |
154 | test('bucketExists() fails due to wrong access key', () async {
155 | final minio = getMinioClient(accessKey: 'incorrect-access-key');
156 | expect(
157 | () async => await minio.bucketExists(bucketName),
158 | throwsA(
159 | isA().having(
160 | (e) => e.message,
161 | 'message',
162 | 'Forbidden',
163 | ),
164 | ),
165 | );
166 | });
167 |
168 | test('bucketExists() fails due to wrong secret key', () async {
169 | final minio = getMinioClient(secretKey: 'incorrect-secret-key');
170 | expect(
171 | () async => await minio.bucketExists(bucketName),
172 | throwsA(
173 | isA().having(
174 | (e) => e.message,
175 | 'message',
176 | 'Forbidden',
177 | ),
178 | ),
179 | );
180 | });
181 | });
182 | }
183 |
184 | void testFPutObject() {
185 | group('fPutObject', () {
186 | final bucketName = uniqueName();
187 | late Directory tempDir;
188 | late File testFile;
189 | final objectName = 'a.jpg';
190 |
191 | setUpAll(() async {
192 | tempDir = await Directory.systemTemp.createTemp();
193 | testFile = await File('${tempDir.path}/$objectName').create();
194 | await testFile.writeAsString('random bytes');
195 |
196 | final minio = getMinioClient();
197 | await minio.makeBucket(bucketName);
198 | });
199 |
200 | tearDownAll(() async {
201 | final minio = getMinioClient();
202 | await minio.removeObject(bucketName, objectName);
203 | await tempDir.delete(recursive: true);
204 | });
205 |
206 | test('fPutObject() inserts content-type to metadata', () async {
207 | final minio = getMinioClient();
208 | await minio.fPutObject(bucketName, objectName, testFile.path);
209 |
210 | final stat = await minio.statObject(bucketName, objectName);
211 | expect(stat.metaData!['content-type'], equals('image/jpeg'));
212 | });
213 |
214 | test('fPutObject() adds user-defined object metadata w/ prefix', () async {
215 | final prefix = 'x-amz-meta-';
216 | final userDefinedMetadataKey = '${prefix}user-defined-metadata-key-1';
217 | final userDefinedMetadataValue = 'custom value 1';
218 | final metadata = {
219 | userDefinedMetadataKey: userDefinedMetadataValue,
220 | };
221 |
222 | final minio = getMinioClient();
223 | await minio.fPutObject(bucketName, objectName, testFile.path, metadata: metadata);
224 |
225 | final stat = await minio.statObject(bucketName, objectName);
226 | expect(
227 | stat.metaData![userDefinedMetadataKey.substring(prefix.length)],
228 | equals(userDefinedMetadataValue),
229 | );
230 | });
231 |
232 | test('fPutObject() adds user-defined object metadata w/o prefix', () async {
233 | final userDefinedMetadataKey = 'user-defined-metadata-key-2';
234 | final userDefinedMetadataValue = 'custom value 2';
235 | final metadata = {
236 | userDefinedMetadataKey: userDefinedMetadataValue,
237 | };
238 |
239 | final minio = getMinioClient();
240 | await minio.fPutObject(bucketName, objectName, testFile.path, metadata: metadata);
241 |
242 | final stat = await minio.statObject(bucketName, objectName);
243 | expect(
244 | stat.metaData![userDefinedMetadataKey],
245 | equals(userDefinedMetadataValue),
246 | );
247 | });
248 |
249 | test('fPutObject() with empty file', () async {
250 | final objectName = 'empty.txt';
251 | final emptyFile = await File('${tempDir.path}/$objectName').create();
252 | await emptyFile.writeAsString('');
253 |
254 | final minio = getMinioClient();
255 | await minio.fPutObject(bucketName, objectName, emptyFile.path);
256 |
257 | final stat = await minio.statObject(bucketName, objectName);
258 | expect(stat.size, equals(0));
259 | });
260 | });
261 | }
262 |
263 | void testSetObjectACL() {
264 | group('setObjectACL', () {
265 | late String bucketName;
266 | late Directory tempDir;
267 | File testFile;
268 | final objectName = 'a.jpg';
269 |
270 | setUpAll(() async {
271 | bucketName = uniqueName();
272 |
273 | tempDir = await Directory.systemTemp.createTemp();
274 | testFile = await File('${tempDir.path}/$objectName').create();
275 | await testFile.writeAsString('random bytes');
276 |
277 | final minio = getMinioClient();
278 | await minio.makeBucket(bucketName);
279 |
280 | await minio.fPutObject(bucketName, objectName, testFile.path);
281 | });
282 |
283 | tearDownAll(() async {
284 | await tempDir.delete(recursive: true);
285 | });
286 |
287 | test('setObjectACL() set objects acl', () async {
288 | final minio = getMinioClient();
289 | await minio.setObjectACL(bucketName, objectName, 'public-read');
290 | });
291 | });
292 | }
293 |
294 | void testGetObjectACL() {
295 | group('getObjectACL', () {
296 | late String bucketName;
297 | late Directory tempDir;
298 | File testFile;
299 | final objectName = 'a.jpg';
300 |
301 | setUpAll(() async {
302 | bucketName = uniqueName();
303 |
304 | tempDir = await Directory.systemTemp.createTemp();
305 | testFile = await File('${tempDir.path}/$objectName').create();
306 | await testFile.writeAsString('random bytes');
307 |
308 | final minio = getMinioClient();
309 | await minio.makeBucket(bucketName);
310 |
311 | await minio.fPutObject(bucketName, objectName, testFile.path);
312 | });
313 |
314 | tearDownAll(() async {
315 | await tempDir.delete(recursive: true);
316 | });
317 |
318 | test('getObjectACL() fetch objects acl', () async {
319 | final minio = getMinioClient();
320 | var acl = await minio.getObjectACL(bucketName, objectName);
321 | expect(acl.grants!.permission, equals(null));
322 | });
323 | });
324 | }
325 |
326 | void testGetObject() {
327 | group('getObject()', () {
328 | final minio = getMinioClient();
329 | final bucketName = uniqueName();
330 | final object = uniqueName();
331 | final objectUtf8 = '${uniqueName()}/あるところ/某个文件.🦐';
332 | final objectData = Uint8List.fromList([1, 2, 3]);
333 |
334 | setUpAll(() async {
335 | await minio.makeBucket(bucketName);
336 | await minio.putObject(bucketName, object, Stream.value(objectData));
337 | await minio.putObject(bucketName, objectUtf8, Stream.value(objectData));
338 | });
339 |
340 | tearDownAll(() async {
341 | await minio.removeObject(bucketName, object);
342 | await minio.removeObject(bucketName, objectUtf8);
343 | await minio.removeBucket(bucketName);
344 | });
345 |
346 | test('succeeds', () async {
347 | final stream = await minio.getObject(bucketName, object);
348 | final buffer = BytesBuilder();
349 | await stream.forEach((data) => buffer.add(data));
350 | expect(stream.contentLength, equals(objectData.length));
351 | expect(buffer.takeBytes(), equals(objectData));
352 | });
353 |
354 | test('succeeds with utf8 object name', () async {
355 | final stream = await minio.getObject(bucketName, object);
356 | final buffer = BytesBuilder();
357 | await stream.forEach((data) => buffer.add(data));
358 | expect(stream.contentLength, equals(objectData.length));
359 | expect(buffer.takeBytes(), equals(objectData));
360 | });
361 |
362 | test('fails on invalid bucket', () {
363 | expect(
364 | () async => await minio.getObject('$bucketName-invalid', object),
365 | throwsA(isA()),
366 | );
367 | });
368 |
369 | test('fails on invalid object', () {
370 | expect(
371 | () async => await minio.getObject(bucketName, '$object-invalid'),
372 | throwsA(isA()),
373 | );
374 | });
375 | });
376 | }
377 |
378 | void testPutObject() {
379 | group('putObject()', () {
380 | final minio = getMinioClient();
381 | final bucketName = uniqueName();
382 | final objectData = Uint8List.fromList([1, 2, 3]);
383 |
384 | setUpAll(() async {
385 | await minio.makeBucket(bucketName);
386 | });
387 |
388 | tearDownAll(() async {
389 | await minio.removeBucket(bucketName);
390 | });
391 |
392 | test('succeeds', () async {
393 | final objectName = uniqueName();
394 | await minio.putObject(bucketName, objectName, Stream.value(objectData));
395 | final stat = await minio.statObject(bucketName, objectName);
396 | expect(stat.size, equals(objectData.length));
397 | await minio.removeObject(bucketName, objectName);
398 | });
399 |
400 | test('works with object names with symbols', () async {
401 | final objectName = uniqueName() + r'-._~,!@#$%^&*()';
402 | await minio.putObject(bucketName, objectName, Stream.value(objectData));
403 | final stat = await minio.statObject(bucketName, objectName);
404 | expect(stat.size, equals(objectData.length));
405 | await minio.removeObject(bucketName, objectName);
406 | });
407 |
408 | test('progress report works', () async {
409 | final objectName = uniqueName();
410 | int? progress;
411 | await minio.putObject(
412 | bucketName,
413 | objectName,
414 | Stream.value(objectData),
415 | onProgress: (bytes) => progress = bytes,
416 | );
417 | await minio.removeObject(bucketName, objectName);
418 | expect(progress, equals(objectData.length));
419 | });
420 |
421 | test('medium size file upload works', () async {
422 | final objectName = uniqueName();
423 | final dataLength = 1024 * 1024;
424 | final data = Uint8List.fromList(List.generate(dataLength, (i) => i));
425 | await minio.putObject(bucketName, objectName, Stream.value(data));
426 | final stat = await minio.statObject(bucketName, objectName);
427 | await minio.removeObject(bucketName, objectName);
428 | expect(stat.size, equals(dataLength));
429 | });
430 |
431 | test('stream upload works', () async {
432 | final objectName = uniqueName();
433 | final dataLength = 1024 * 1024;
434 | final data = Uint8List.fromList(List.generate(dataLength, (i) => i));
435 | await minio.putObject(
436 | bucketName,
437 | objectName,
438 | Stream.value(data).transform(MaxChunkSize(123)),
439 | );
440 | final stat = await minio.statObject(bucketName, objectName);
441 | await minio.removeObject(bucketName, objectName);
442 | expect(stat.size, equals(dataLength));
443 | });
444 |
445 | test('empty stream upload works', () async {
446 | final objectName = uniqueName();
447 | await minio.putObject(bucketName, objectName, const Stream.empty());
448 | final stat = await minio.statObject(bucketName, objectName);
449 | await minio.removeObject(bucketName, objectName);
450 | expect(stat.size, equals(0));
451 | });
452 |
453 | test('zero byte stream upload works', () async {
454 | final objectName = uniqueName();
455 | await minio.putObject(bucketName, objectName, Stream.value(Uint8List(0)));
456 | final stat = await minio.statObject(bucketName, objectName);
457 | await minio.removeObject(bucketName, objectName);
458 | expect(stat.size, equals(0));
459 | });
460 |
461 | test('multipart file upload works', () async {
462 | final objectName = uniqueName();
463 | final dataLength = 12 * 1024 * 1024;
464 | final data = Uint8List.fromList(List.generate(dataLength, (i) => i));
465 | await minio.putObject(
466 | bucketName,
467 | objectName,
468 | Stream.value(data),
469 | chunkSize: 5 * 1024 * 1024,
470 | );
471 | final stat = await minio.statObject(bucketName, objectName);
472 | await minio.removeObject(bucketName, objectName);
473 | expect(stat.size, equals(dataLength));
474 | });
475 | });
476 | }
477 |
478 | void testGetBucketNotification() {
479 | group('getBucketNotification()', () {
480 | final minio = getMinioClient();
481 | final bucketName = uniqueName();
482 |
483 | setUpAll(() async {
484 | await minio.makeBucket(bucketName);
485 | });
486 |
487 | tearDownAll(() async {
488 | await minio.removeBucket(bucketName);
489 | });
490 |
491 | test('succeeds', () async {
492 | await minio.getBucketNotification(bucketName);
493 | });
494 | });
495 | }
496 |
497 | void testSetBucketNotification() {
498 | group('setBucketNotification()', () {
499 | final minio = getMinioClient();
500 | final bucketName = uniqueName();
501 |
502 | setUpAll(() async {
503 | await minio.makeBucket(bucketName);
504 | });
505 |
506 | tearDownAll(() async {
507 | await minio.removeBucket(bucketName);
508 | });
509 |
510 | test('succeeds', () async {
511 | await minio.setBucketNotification(
512 | bucketName,
513 | NotificationConfiguration(null, null, null),
514 | );
515 | });
516 | });
517 | }
518 |
519 | void testRemoveAllBucketNotification() {
520 | group('removeAllBucketNotification()', () {
521 | final minio = getMinioClient();
522 | final bucketName = uniqueName();
523 |
524 | setUpAll(() async {
525 | await minio.makeBucket(bucketName);
526 | });
527 |
528 | tearDownAll(() async {
529 | await minio.removeBucket(bucketName);
530 | });
531 |
532 | test('succeeds', () async {
533 | await minio.removeAllBucketNotification(bucketName);
534 | });
535 | });
536 | }
537 |
538 | void testListenBucketNotification() {
539 | group('listenBucketNotification()', () {
540 | final minio = getMinioClient();
541 | final bucketName = uniqueName();
542 | // final objectName = uniqueName();
543 |
544 | setUpAll(() async {
545 | await minio.makeBucket(bucketName);
546 | });
547 |
548 | tearDownAll(() async {
549 | await minio.removeBucket(bucketName);
550 | });
551 |
552 | test('succeeds', () async {
553 | final poller = minio.listenBucketNotification(bucketName);
554 | expect(poller.isStarted, isTrue);
555 | poller.stop();
556 | });
557 |
558 | // test('can receive notification', () async {
559 | // final poller = minio.listenBucketNotification(
560 | // bucketName,
561 | // events: ['s3:ObjectCreated:*'],
562 | // );
563 |
564 | // final receivedEvents = [];
565 | // poller.stream.listen((event) => receivedEvents.add(event));
566 | // expect(receivedEvents, isEmpty);
567 |
568 | // await minio.putObject(bucketName, objectName, Stream.value([0]));
569 | // await minio.removeObject(bucketName, objectName);
570 |
571 | // // FIXME: Needs sleep here
572 | // expect(receivedEvents, isNotEmpty);
573 |
574 | // poller.stop();
575 | // });
576 | });
577 | }
578 |
579 | void testStatObject() {
580 | group('statObject()', () {
581 | final minio = getMinioClient();
582 | final bucketName = uniqueName();
583 | final object = uniqueName();
584 | final objectUtf8 = '${uniqueName()}オブジェクト。📦';
585 | final data = Uint8List.fromList([1, 2, 3, 4, 5]);
586 |
587 | setUpAll(() async {
588 | await minio.makeBucket(bucketName);
589 | await minio.putObject(bucketName, object, Stream.value(data));
590 | await minio.putObject(bucketName, objectUtf8, Stream.value(data));
591 | });
592 |
593 | tearDownAll(() async {
594 | await minio.removeObject(bucketName, object);
595 | await minio.removeObject(bucketName, objectUtf8);
596 | await minio.removeBucket(bucketName);
597 | });
598 |
599 | test('succeeds', () async {
600 | final stats = await minio.statObject(bucketName, object);
601 | expect(stats.lastModified, isNotNull);
602 | expect(stats.lastModified!.isBefore(DateTime.now()), isTrue);
603 | expect(stats.size, isNotNull);
604 | expect(stats.size, equals(data.length));
605 | });
606 |
607 | test('succeeds with utf8 object name', () async {
608 | final stats = await minio.statObject(bucketName, objectUtf8);
609 | expect(stats.lastModified, isNotNull);
610 | expect(stats.lastModified!.isBefore(DateTime.now()), isTrue);
611 | expect(stats.size, isNotNull);
612 | expect(stats.size, equals(data.length));
613 | });
614 |
615 | test('fails on invalid bucket', () {
616 | expect(
617 | () async => await minio.statObject('$bucketName-invalid', object),
618 | throwsA(isA()),
619 | );
620 | });
621 |
622 | test('fails on invalid object', () {
623 | expect(
624 | () async => await minio.statObject(bucketName, '$object-invalid'),
625 | throwsA(isA()),
626 | );
627 | });
628 | });
629 | }
630 |
631 | void testMakeBucket() {
632 | group('makeBucket()', () {
633 | final minio = getMinioClient();
634 | final bucketName = uniqueName();
635 |
636 | setUpAll(() async {
637 | await minio.makeBucket(bucketName);
638 | });
639 |
640 | tearDownAll(() async {
641 | await minio.removeBucket(bucketName);
642 | });
643 |
644 | test('succeeds', () async {
645 | final buckets = await minio.listBuckets();
646 | final bucketNames = buckets.map((b) => b.name).toList();
647 | expect(bucketNames, contains(bucketName));
648 | });
649 | });
650 | }
651 |
652 | void testRemoveBucket() {
653 | group('removeBucket()', () {
654 | final minio = getMinioClient();
655 | final bucketName = uniqueName();
656 |
657 | test('succeeds', () async {
658 | await minio.makeBucket(bucketName);
659 | await minio.removeBucket(bucketName);
660 | });
661 |
662 | test('fails on invalid bucket name', () {
663 | expect(
664 | () async => await minio.removeBucket('$bucketName-invalid'),
665 | throwsA(isA()),
666 | );
667 | });
668 | });
669 | }
670 |
671 | void testRemoveObject() {
672 | group('removeObject()', () {
673 | final minio = getMinioClient();
674 | final bucketName = uniqueName();
675 | final objectName = uniqueName();
676 | final data = Uint8List.fromList([1, 2, 3, 4, 5]);
677 |
678 | setUpAll(() async {
679 | await minio.makeBucket(bucketName);
680 | });
681 |
682 | tearDownAll(() async {
683 | await minio.removeBucket(bucketName);
684 | });
685 |
686 | test('succeeds', () async {
687 | await minio.putObject(bucketName, objectName, Stream.value(data));
688 | await minio.removeObject(bucketName, objectName);
689 |
690 | await for (var chunk in minio.listObjects(bucketName)) {
691 | expect(chunk.objects.map((e) => e.key).contains(objectName), isFalse);
692 | }
693 | });
694 |
695 | test('fails on invalid bucket', () {
696 | expect(
697 | () async => await minio.removeObject('$bucketName-invalid', objectName),
698 | throwsA(isA()),
699 | );
700 | });
701 |
702 | test('does not throw on invalid object', () async {
703 | await minio.removeObject(bucketName, '$objectName-invalid');
704 | });
705 | });
706 | }
707 |
708 | void testListObjects() {
709 | group('listAllObjects()', () {
710 | final minio = getMinioClient();
711 | final bucketName = uniqueName();
712 | final objectName = uniqueName();
713 | final objectNameUtf8 = '${uniqueName()}文件ファイル。ㄴㅁㄴ';
714 | final data = Uint8List.fromList([1, 2, 3, 4, 5]);
715 |
716 | setUpAll(() async {
717 | await minio.makeBucket(bucketName);
718 | await minio.putObject(bucketName, objectName, Stream.value(data));
719 | await minio.putObject(bucketName, objectNameUtf8, Stream.value(data));
720 | });
721 |
722 | tearDownAll(() async {
723 | await minio.removeObject(bucketName, objectName);
724 | await minio.removeObject(bucketName, objectNameUtf8);
725 | await minio.removeBucket(bucketName);
726 | });
727 |
728 | test('succeeds', () async {
729 | final result = await minio.listAllObjects(bucketName);
730 | print(result);
731 | expect(result.objects.map((e) => e.key).contains(objectName), isTrue);
732 | expect(result.objects.map((e) => e.key).contains(objectNameUtf8), isTrue);
733 | });
734 |
735 | test('fails on invalid bucket', () {
736 | expect(
737 | () async => await minio.listAllObjects('$bucketName-invalid'),
738 | throwsA(isA()),
739 | );
740 | });
741 | });
742 |
743 | group('listAllObjects() works when prefix contains spaces', () {
744 | final minio = getMinioClient();
745 | final bucket = uniqueName();
746 | final object = 'new folder/new file.txt';
747 | final data = Uint8List.fromList([1, 2, 3, 4, 5]);
748 |
749 | setUpAll(() async {
750 | await minio.makeBucket(bucket);
751 | await minio.putObject(bucket, object, Stream.value(data));
752 | });
753 |
754 | tearDownAll(() async {
755 | await minio.removeObject(bucket, object);
756 | await minio.removeBucket(bucket);
757 | });
758 |
759 | test('succeeds', () async {
760 | final result = await minio.listAllObjects(bucket, prefix: 'new folder/');
761 | expect(result.objects.map((e) => e.key).contains(object), isTrue);
762 | });
763 | });
764 |
765 | group('listAllObjects() works when prefix contains utf-8 characters', () {
766 | final minio = getMinioClient();
767 | final bucket = uniqueName();
768 | final prefix = '🍎🌰🍌🍓/文件夹 1 2/';
769 | final object = '${prefix}new file.txt';
770 | final data = Uint8List.fromList([1, 2, 3, 4, 5]);
771 |
772 | setUpAll(() async {
773 | await minio.makeBucket(bucket);
774 | await minio.putObject(bucket, object, Stream.value(data));
775 | });
776 |
777 | tearDownAll(() async {
778 | await minio.removeObject(bucket, object);
779 | await minio.removeBucket(bucket);
780 | });
781 |
782 | test('succeeds', () async {
783 | final result = await minio.listAllObjects(bucket, prefix: prefix);
784 | print(result);
785 | expect(result.objects.map((e) => e.key).contains(object), isTrue);
786 | });
787 | });
788 | }
789 |
--------------------------------------------------------------------------------
/lib/src/minio.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: deprecated_member_use
2 |
3 | import 'dart:async';
4 | import 'dart:convert';
5 | import 'dart:typed_data';
6 |
7 | import 'package:minio/src/minio_client.dart';
8 | import 'package:minio/src/minio_errors.dart';
9 | import 'package:minio/src/minio_helpers.dart';
10 | import 'package:minio/src/minio_models.dart';
11 | import 'package:minio/src/minio_models_generated.dart';
12 | import 'package:minio/src/minio_poller.dart';
13 | import 'package:minio/src/minio_sign.dart';
14 | import 'package:minio/src/minio_stream.dart';
15 | import 'package:minio/src/minio_uploader.dart';
16 | import 'package:minio/src/utils.dart';
17 | import 'package:xml/xml.dart' as xml;
18 | import 'package:xml/xml.dart' show XmlElement;
19 |
20 | class Minio {
21 | /// Initializes a new client object.
22 | Minio({
23 | required this.endPoint,
24 | required this.accessKey,
25 | required this.secretKey,
26 | int? port,
27 | this.useSSL = true,
28 | this.sessionToken,
29 | this.region,
30 | this.pathStyle,
31 | this.enableTrace = false,
32 | }) : port = port ?? implyPort(useSSL) {
33 | if (!isValidEndpoint(endPoint)) {
34 | throw MinioInvalidEndpointError(
35 | 'End point $endPoint is not a valid domain or ip address',
36 | );
37 | }
38 |
39 | if (!isValidPort(this.port)) {
40 | throw MinioInvalidPortError(
41 | 'Invalid port number ${this.port}',
42 | );
43 | }
44 |
45 | _client = MinioClient(this);
46 | }
47 |
48 | /// default part size for multipart uploads.
49 | final partSize = 64 * 1024 * 1024;
50 |
51 | /// maximum part size for multipart uploads.
52 | final maximumPartSize = 5 * 1024 * 1024 * 1024;
53 |
54 | /// maximum object size (5TB)
55 | final maxObjectSize = 5 * 1024 * 1024 * 1024 * 1024;
56 |
57 | /// endPoint is a host name or an IP address.
58 | ///
59 | /// For example:
60 | /// - play.min.io
61 | /// - 1.2.3.4
62 | final String endPoint;
63 |
64 | /// TCP/IP port number. This input is optional. Default value set to 80 for HTTP and 443 for HTTPs.
65 | final int port;
66 |
67 | /// If set to true, https is used instead of http. Default is true.
68 | final bool useSSL;
69 |
70 | /// accessKey is like user-id that uniquely identifies your account.
71 | final String accessKey;
72 |
73 | /// secretKey is the password to your account.
74 | final String secretKey;
75 |
76 | /// Set this value to provide x-amz-security-token (AWS S3 specific). (Optional)
77 | final String? sessionToken;
78 |
79 | /// Set this value to override region cache. (Optional)
80 | final String? region;
81 |
82 | /// Set this value to override default access behavior (path) for non AWS endpoints. Default is true. (Optional)
83 | final bool? pathStyle;
84 |
85 | /// Set this value to enable tracing. (Optional)
86 | final bool enableTrace;
87 |
88 | late MinioClient _client;
89 | final _regionMap = {};
90 |
91 | /// Checks if a bucket exists.
92 | ///
93 | /// Returns `true` only if the [bucket] exists and you have the permission
94 | /// to access it. Returns `false` if the [bucket] does not exist or you
95 | /// don't have the permission to access it.
96 | Future bucketExists(String bucket) async {
97 | MinioInvalidBucketNameError.check(bucket);
98 | try {
99 | final response = await _client.request(method: 'HEAD', bucket: bucket);
100 | validate(response);
101 | return response.statusCode == 200;
102 | } on MinioS3Error catch (e) {
103 | final code = e.error?.code;
104 | if (code == 'NoSuchBucket' || code == 'NotFound' || code == 'Not Found') {
105 | return false;
106 | }
107 | rethrow;
108 | } on StateError catch (e) {
109 | // Insight from testing: in most cases, AWS S3 returns the HTTP status code
110 | // 404 when a bucket does not exist. Whereas in other cases, when the bucket
111 | // does not exist, S3 returns the HTTP status code 301 Redirect instead of
112 | // status code 404 as officially documented. Then, this redirect response
113 | // lacks the HTTP header `location` which causes this exception in Dart's
114 | // HTTP library (`http_impl.dart`).
115 | if (e.message == 'Response has no Location header for redirect') {
116 | return false;
117 | }
118 | rethrow;
119 | }
120 | }
121 |
122 | int _calculatePartSize(int size) {
123 | assert(size >= 0);
124 |
125 | if (size > maxObjectSize) {
126 | throw ArgumentError('size should not be more than $maxObjectSize');
127 | }
128 |
129 | var partSize = this.partSize;
130 | while (true) {
131 | if ((partSize * 10000) > size) {
132 | return partSize;
133 | }
134 | partSize += 16 * 1024 * 1024;
135 | }
136 | }
137 |
138 | /// Complete the multipart upload. After all the parts are uploaded issuing
139 | /// this call will aggregate the parts on the server into a single object.
140 | Future completeMultipartUpload(
141 | String bucket,
142 | String object,
143 | String uploadId,
144 | List parts,
145 | ) async {
146 | MinioInvalidBucketNameError.check(bucket);
147 | MinioInvalidObjectNameError.check(object);
148 |
149 | var queries = {'uploadId': uploadId};
150 | var payload = CompleteMultipartUpload(parts).toXml().toString();
151 |
152 | final resp = await _client.request(
153 | method: 'POST',
154 | bucket: bucket,
155 | object: object,
156 | queries: queries,
157 | payload: payload,
158 | );
159 | validate(resp, expect: 200);
160 |
161 | final node = xml.XmlDocument.parse(resp.body);
162 | final errorNode = node.findAllElements('Error');
163 | if (errorNode.isNotEmpty) {
164 | final error = Error.fromXml(errorNode.first);
165 | throw MinioS3Error(error.message, error, resp);
166 | }
167 |
168 | final etag = node.findAllElements('ETag').first.text;
169 | return etag;
170 | }
171 |
172 | /// Copy the object.
173 | Future copyObject(
174 | String bucket,
175 | String object,
176 | String srcObject, [
177 | CopyConditions? conditions,
178 | ]) async {
179 | MinioInvalidBucketNameError.check(bucket);
180 | MinioInvalidObjectNameError.check(object);
181 | MinioInvalidObjectNameError.check(srcObject);
182 |
183 | final headers = {};
184 | headers['x-amz-copy-source'] = srcObject;
185 |
186 | if (conditions != null) {
187 | if (conditions.modified != null) {
188 | headers['x-amz-copy-source-if-modified-since'] = conditions.modified!;
189 | }
190 | if (conditions.unmodified != null) {
191 | headers['x-amz-copy-source-if-unmodified-since'] =
192 | conditions.unmodified!;
193 | }
194 | if (conditions.matchETag != null) {
195 | headers['x-amz-copy-source-if-match'] = conditions.matchETag!;
196 | }
197 | if (conditions.matchETagExcept != null) {
198 | headers['x-amz-copy-source-if-none-match'] =
199 | conditions.matchETagExcept!;
200 | }
201 | }
202 |
203 | final resp = await _client.request(
204 | method: 'PUT',
205 | bucket: bucket,
206 | object: object,
207 | headers: headers,
208 | );
209 |
210 | validate(resp);
211 |
212 | final node = xml.XmlDocument.parse(resp.body);
213 | final result = CopyObjectResult.fromXml(node.rootElement);
214 | result.eTag = trimDoubleQuote(result.eTag!);
215 | return result;
216 | }
217 |
218 | /// Find uploadId of an incomplete upload.
219 | Future findUploadId(String bucket, String object) async {
220 | MinioInvalidBucketNameError.check(bucket);
221 | MinioInvalidObjectNameError.check(object);
222 |
223 | MultipartUpload? latestUpload;
224 | String? keyMarker;
225 | String? uploadIdMarker;
226 | bool? isTruncated = false;
227 |
228 | do {
229 | final result = await listIncompleteUploadsQuery(
230 | bucket,
231 | object,
232 | keyMarker,
233 | uploadIdMarker,
234 | '',
235 | );
236 | for (var upload in result.uploads) {
237 | if (upload.key != object) continue;
238 | if (latestUpload == null ||
239 | upload.initiated!.isAfter(latestUpload.initiated!)) {
240 | latestUpload = upload;
241 | }
242 | }
243 | keyMarker = result.nextKeyMarker;
244 | uploadIdMarker = result.nextUploadIdMarker;
245 | isTruncated = result.isTruncated;
246 | } while (isTruncated!);
247 |
248 | return latestUpload?.uploadId;
249 | }
250 |
251 | /// Return the list of notification configurations stored
252 | /// in the S3 provider
253 | Future getBucketNotification(String bucket) async {
254 | MinioInvalidBucketNameError.check(bucket);
255 |
256 | final resp = await _client.request(
257 | method: 'GET',
258 | bucket: bucket,
259 | resource: 'notification',
260 | );
261 |
262 | validate(resp, expect: 200);
263 |
264 | final node = xml.XmlDocument.parse(resp.body);
265 | return NotificationConfiguration.fromXml(node.rootElement);
266 | }
267 |
268 | /// Get the bucket policy associated with the specified bucket. If `objectPrefix`
269 | /// is not empty, the bucket policy will be filtered based on object permissions
270 | /// as well.
271 | Future?> getBucketPolicy(bucket) async {
272 | MinioInvalidBucketNameError.check(bucket);
273 |
274 | final resp = await _client.request(
275 | method: 'GET',
276 | bucket: bucket,
277 | resource: 'policy',
278 | );
279 |
280 | validate(resp, expect: 200);
281 |
282 | return json.decode(resp.body);
283 | }
284 |
285 | /// Gets the region of [bucket]. The region is cached for subsequent calls.
286 | Future getBucketRegion(String bucket) async {
287 | MinioInvalidBucketNameError.check(bucket);
288 |
289 | if (region != null) {
290 | return region!;
291 | }
292 |
293 | if (_regionMap.containsKey(bucket)) {
294 | return _regionMap[bucket]!;
295 | }
296 |
297 | final resp = await _client.request(
298 | method: 'GET',
299 | bucket: bucket,
300 | region: 'us-east-1',
301 | queries: {'location': null},
302 | );
303 |
304 | validate(resp);
305 |
306 | final node = xml.XmlDocument.parse(resp.body);
307 |
308 | var location = node.findAllElements('LocationConstraint').first.text;
309 | // if (location == null || location.isEmpty) {
310 | if (location.isEmpty) {
311 | location = 'us-east-1';
312 | }
313 |
314 | _regionMap[bucket] = location;
315 | return location;
316 | }
317 |
318 | /// get a readable stream of the object content.
319 | Future getObject(String bucket, String object) {
320 | return getPartialObject(bucket, object, null, null);
321 | }
322 |
323 | /// get a readable stream of the partial object content.
324 | Future getPartialObject(
325 | String bucket,
326 | String object, [
327 | int? offset,
328 | int? length,
329 | ]) async {
330 | assert(offset == null || offset >= 0);
331 | assert(length == null || length > 0);
332 |
333 | MinioInvalidBucketNameError.check(bucket);
334 | MinioInvalidObjectNameError.check(object);
335 |
336 | String? range;
337 | if (offset != null || length != null) {
338 | if (offset != null) {
339 | range = 'bytes=$offset-';
340 | } else {
341 | range = 'bytes=0-';
342 | offset = 0;
343 | }
344 | if (length != null) {
345 | range += '${(length + offset) - 1}';
346 | }
347 | }
348 |
349 | final headers = range != null ? {'range': range} : null;
350 | final expectedStatus = range != null ? 206 : 200;
351 |
352 | final resp = await _client.requestStream(
353 | method: 'GET',
354 | bucket: bucket,
355 | object: object,
356 | headers: headers,
357 | );
358 |
359 | await validateStreamed(resp, expect: expectedStatus);
360 |
361 | return MinioByteStream.fromStream(
362 | stream: resp.stream,
363 | contentLength: resp.contentLength,
364 | );
365 | }
366 |
367 | /// Initiate a new multipart upload.
368 | Future initiateNewMultipartUpload(
369 | String bucket,
370 | String object,
371 | Map? metaData,
372 | ) async {
373 | MinioInvalidBucketNameError.check(bucket);
374 | MinioInvalidObjectNameError.check(object);
375 |
376 | final resp = await _client.request(
377 | method: 'POST',
378 | bucket: bucket,
379 | object: object,
380 | headers: metaData,
381 | resource: 'uploads',
382 | );
383 |
384 | validate(resp, expect: 200);
385 |
386 | final node = xml.XmlDocument.parse(resp.body);
387 | return node.findAllElements('UploadId').first.text;
388 | }
389 |
390 | /// Returns a stream that emits objects that are partially uploaded.
391 | Stream listIncompleteUploads(
392 | String bucket,
393 | String prefix, [
394 | bool recursive = false,
395 | ]) async* {
396 | MinioInvalidBucketNameError.check(bucket);
397 | MinioInvalidPrefixError.check(prefix);
398 |
399 | final delimiter = recursive ? '' : '/';
400 |
401 | String? keyMarker;
402 | String? uploadIdMarker;
403 | var isTruncated = false;
404 |
405 | do {
406 | final result = await listIncompleteUploadsQuery(
407 | bucket,
408 | prefix,
409 | keyMarker,
410 | uploadIdMarker,
411 | delimiter,
412 | );
413 | for (var upload in result.uploads) {
414 | final parts = listParts(bucket, upload.key!, upload.uploadId!);
415 | final size =
416 | await parts.fold(0, (dynamic acc, item) => acc + item.size);
417 | yield IncompleteUpload(upload: upload, size: size);
418 | }
419 | keyMarker = result.nextKeyMarker;
420 | uploadIdMarker = result.nextUploadIdMarker;
421 | isTruncated = result.isTruncated!;
422 | } while (isTruncated);
423 | }
424 |
425 | /// Called by listIncompleteUploads to fetch a batch of incomplete uploads.
426 | Future listIncompleteUploadsQuery(
427 | String bucket,
428 | String prefix,
429 | String? keyMarker,
430 | String? uploadIdMarker,
431 | String delimiter,
432 | ) async {
433 | MinioInvalidBucketNameError.check(bucket);
434 | MinioInvalidPrefixError.check(prefix);
435 |
436 | var queries = {
437 | 'uploads': null,
438 | 'prefix': prefix,
439 | 'delimiter': delimiter,
440 | };
441 |
442 | if (keyMarker != null) {
443 | queries['key-marker'] = keyMarker;
444 | }
445 | if (uploadIdMarker != null) {
446 | queries['upload-id-marker'] = uploadIdMarker;
447 | }
448 |
449 | final resp = await _client.request(
450 | method: 'GET',
451 | bucket: bucket,
452 | resource: 'uploads',
453 | queries: queries,
454 | );
455 |
456 | validate(resp);
457 |
458 | final node = xml.XmlDocument.parse(resp.body);
459 | return ListMultipartUploadsOutput.fromXml(node.root as XmlElement);
460 | }
461 |
462 | /// Listen for notifications on a bucket. Additionally one can provider
463 | /// filters for prefix, suffix and events. There is no prior set bucket notification
464 | /// needed to use this API. **This is an MinIO extension API** where unique identifiers
465 | /// are regitered and unregistered by the server automatically based on incoming requests.
466 | NotificationPoller listenBucketNotification(
467 | String bucket, {
468 | String? prefix,
469 | String? suffix,
470 | List? events,
471 | }) {
472 | MinioInvalidBucketNameError.check(bucket);
473 |
474 | final poller = NotificationPoller(_client, bucket, prefix, suffix, events);
475 |
476 | poller.start();
477 |
478 | return poller;
479 | }
480 |
481 | /// List of buckets created.
482 | Future> listBuckets() async {
483 | final resp = await _client.request(
484 | method: 'GET',
485 | region: region ?? 'us-east-1',
486 | );
487 | validate(resp);
488 | final bucketsNode =
489 | xml.XmlDocument.parse(resp.body).findAllElements('Buckets').first;
490 | return bucketsNode.children
491 | .map((n) => Bucket.fromXml(n as XmlElement))
492 | .toList();
493 | }
494 |
495 | /// Returns all [Object]s in a bucket.
496 | /// To list objects in a bucket with prefix, set [prefix] to the desired prefix.
497 | Stream listObjects(
498 | String bucket, {
499 | String prefix = '',
500 | bool recursive = false,
501 | }) async* {
502 | MinioInvalidBucketNameError.check(bucket);
503 | MinioInvalidPrefixError.check(prefix);
504 | final delimiter = recursive ? '' : '/';
505 |
506 | String? marker;
507 | var isTruncated = false;
508 |
509 | do {
510 | final resp = await listObjectsQuery(
511 | bucket,
512 | prefix,
513 | marker,
514 | delimiter,
515 | 1000,
516 | );
517 | isTruncated = resp.isTruncated!;
518 | marker = resp.nextMarker;
519 | yield ListObjectsResult(
520 | objects: resp.contents!,
521 | prefixes: resp.commonPrefixes.map((e) => e.prefix!).toList(),
522 | );
523 | } while (isTruncated);
524 | }
525 |
526 | /// Returns all [Object]s in a bucket. This is a shortcut for [listObjects].
527 | /// Use [listObjects] to list buckets with a large number of objects.
528 | Future listAllObjects(
529 | String bucket, {
530 | String prefix = '',
531 | bool recursive = false,
532 | }) async {
533 | final chunks = listObjects(bucket, prefix: prefix, recursive: recursive);
534 | final objects = [];
535 | final prefixes = [];
536 | await for (final chunk in chunks) {
537 | objects.addAll(chunk.objects);
538 | prefixes.addAll(chunk.prefixes);
539 | }
540 | return ListObjectsResult(
541 | objects: objects,
542 | prefixes: prefixes,
543 | );
544 | }
545 |
546 | /// list a batch of objects
547 | Future listObjectsQuery(
548 | String bucket,
549 | String prefix,
550 | String? marker,
551 | String delimiter,
552 | int? maxKeys,
553 | ) async {
554 | MinioInvalidBucketNameError.check(bucket);
555 | MinioInvalidPrefixError.check(prefix);
556 |
557 | final queries = {};
558 | queries['prefix'] = prefix;
559 | queries['delimiter'] = delimiter;
560 |
561 | if (marker != null) {
562 | queries['marker'] = marker;
563 | }
564 |
565 | if (maxKeys != null) {
566 | maxKeys = maxKeys >= 1000 ? 1000 : maxKeys;
567 | queries['max-keys'] = maxKeys.toString();
568 | }
569 |
570 | final resp = await _client.request(
571 | method: 'GET',
572 | bucket: bucket,
573 | queries: queries,
574 | );
575 |
576 | validate(resp);
577 |
578 | final node = xml.XmlDocument.parse(resp.body);
579 | final isTruncated = getNodeProp(node.rootElement, 'IsTruncated')!.text;
580 | final nextMarker = getNodeProp(node.rootElement, 'NextMarker')?.text;
581 | final objs = node.findAllElements('Contents').map((c) => Object.fromXml(c));
582 | final prefixes = node
583 | .findAllElements('CommonPrefixes')
584 | .map((c) => CommonPrefix.fromXml(c));
585 |
586 | return ListObjectsOutput()
587 | ..contents = objs.toList()
588 | ..commonPrefixes = prefixes.toList()
589 | ..isTruncated = isTruncated.toLowerCase() == 'true'
590 | ..nextMarker = nextMarker;
591 | }
592 |
593 | /// Returns all [Object]s in a bucket.
594 | /// To list objects in a bucket with prefix, set [prefix] to the desired prefix.
595 | /// This uses ListObjectsV2 in the S3 API. For backward compatibility, use
596 | /// [listObjects] instead.
597 | Stream listObjectsV2(
598 | String bucket, {
599 | String prefix = '',
600 | bool recursive = false,
601 | String? startAfter,
602 | }) async* {
603 | MinioInvalidBucketNameError.check(bucket);
604 | MinioInvalidPrefixError.check(prefix);
605 | final delimiter = recursive ? '' : '/';
606 |
607 | bool? isTruncated = false;
608 | String? continuationToken;
609 |
610 | do {
611 | final resp = await listObjectsV2Query(
612 | bucket,
613 | prefix,
614 | continuationToken,
615 | delimiter,
616 | 1000,
617 | startAfter,
618 | );
619 | isTruncated = resp.isTruncated;
620 | continuationToken = resp.nextContinuationToken;
621 | yield ListObjectsResult(
622 | objects: resp.contents!,
623 | prefixes: resp.commonPrefixes.map((e) => e.prefix!).toList(),
624 | );
625 | } while (isTruncated!);
626 | }
627 |
628 | /// Returns all [Object]s in a bucket. This is a shortcut for [listObjectsV2].
629 | /// Use [listObjects] to list buckets with a large number of objects.
630 | /// This uses ListObjectsV2 in the S3 API. For backward compatibility, use
631 | /// [listAllObjects] instead.
632 | Future listAllObjectsV2(
633 | String bucket, {
634 | String prefix = '',
635 | bool recursive = false,
636 | }) async {
637 | final chunks = listObjects(bucket, prefix: prefix, recursive: recursive);
638 | final objects = [];
639 | final prefixes = [];
640 | await for (final chunk in chunks) {
641 | objects.addAll(chunk.objects);
642 | prefixes.addAll(chunk.prefixes);
643 | }
644 | return ListObjectsResult(
645 | objects: objects,
646 | prefixes: prefixes,
647 | );
648 | }
649 |
650 | /// listObjectsV2Query - (List Objects V2) - List some or all (up to 1000) of the objects in a bucket.
651 | Future listObjectsV2Query(
652 | String bucket,
653 | String prefix,
654 | String? continuationToken,
655 | String delimiter,
656 | int? maxKeys,
657 | String? startAfter,
658 | ) async {
659 | MinioInvalidBucketNameError.check(bucket);
660 | MinioInvalidPrefixError.check(prefix);
661 |
662 | final queries = {};
663 | queries['prefix'] = prefix;
664 | queries['delimiter'] = delimiter;
665 | queries['list-type'] = '2';
666 |
667 | if (continuationToken != null) {
668 | queries['continuation-token'] = continuationToken;
669 | }
670 |
671 | if (startAfter != null) {
672 | queries['start-after'] = startAfter;
673 | }
674 |
675 | if (maxKeys != null) {
676 | maxKeys = maxKeys >= 1000 ? 1000 : maxKeys;
677 | queries['max-keys'] = maxKeys.toString();
678 | }
679 |
680 | final resp = await _client.request(
681 | method: 'GET',
682 | bucket: bucket,
683 | queries: queries,
684 | );
685 |
686 | validate(resp);
687 |
688 | final node = xml.XmlDocument.parse(resp.body);
689 | final isTruncated = getNodeProp(node.rootElement, 'IsTruncated')!.text;
690 | final nextContinuationToken =
691 | getNodeProp(node.rootElement, 'NextContinuationToken')?.text;
692 | final objs = node.findAllElements('Contents').map((c) => Object.fromXml(c));
693 | final prefixes = node
694 | .findAllElements('CommonPrefixes')
695 | .map((c) => CommonPrefix.fromXml(c));
696 |
697 | return ListObjectsV2Output()
698 | ..contents = objs.toList()
699 | ..commonPrefixes = prefixes.toList()
700 | ..isTruncated = isTruncated.toLowerCase() == 'true'
701 | ..nextContinuationToken = nextContinuationToken;
702 | }
703 |
704 | /// Get part-info of all parts of an incomplete upload specified by uploadId.
705 | Stream listParts(
706 | String bucket,
707 | String object,
708 | String uploadId,
709 | ) async* {
710 | MinioInvalidBucketNameError.check(bucket);
711 | MinioInvalidObjectNameError.check(object);
712 |
713 | var marker = 0;
714 | var isTruncated = false;
715 | do {
716 | final result = await listPartsQuery(bucket, object, uploadId, marker);
717 | marker = result.nextPartNumberMarker;
718 | isTruncated = result.isTruncated;
719 | yield* Stream.fromIterable(result.parts);
720 | } while (isTruncated);
721 | }
722 |
723 | /// Called by listParts to fetch a batch of part-info
724 | Future listPartsQuery(
725 | String? bucket,
726 | String? object,
727 | String? uploadId,
728 | int? marker,
729 | ) async {
730 | var queries = {'uploadId': uploadId};
731 |
732 | if (marker != null && marker != 0) {
733 | queries['part-number-marker'] = marker.toString();
734 | }
735 |
736 | final resp = await _client.request(
737 | method: 'GET',
738 | bucket: bucket,
739 | object: object,
740 | queries: queries,
741 | );
742 |
743 | validate(resp);
744 |
745 | final node = xml.XmlDocument.parse(resp.body);
746 | return ListPartsOutput.fromXml(node.root as XmlElement);
747 | }
748 |
749 | /// Creates the bucket [bucket].
750 | Future makeBucket(String bucket, [String? region]) async {
751 | MinioInvalidBucketNameError.check(bucket);
752 | if (this.region != null && region != null && this.region != region) {
753 | throw MinioInvalidArgumentError(
754 | 'Configured region ${this.region}, requested $region',
755 | );
756 | }
757 |
758 | region ??= this.region ?? 'us-east-1';
759 | final payload = region == 'us-east-1'
760 | ? ''
761 | : CreateBucketConfiguration(region).toXml().toString();
762 |
763 | final resp = await _client.request(
764 | method: 'PUT',
765 | bucket: bucket,
766 | region: region,
767 | payload: payload,
768 | );
769 |
770 | validate(resp);
771 | // return resp.body;
772 | }
773 |
774 | /// Generate a presigned URL for GET
775 | ///
776 | /// - [bucketName]: name of the bucket
777 | /// - [objectName]: name of the object
778 | /// - [expires]: expiry in seconds (optional, default 7 days)
779 | /// - [respHeaders]: response headers to override (optional)
780 | /// - [requestDate]: A date object, the url will be issued at (optional)
781 | Future presignedGetObject(
782 | String bucket,
783 | String object, {
784 | int? expires,
785 | Map? respHeaders,
786 | DateTime? requestDate,
787 | }) {
788 | MinioInvalidBucketNameError.check(bucket);
789 | MinioInvalidObjectNameError.check(object);
790 |
791 | return presignedUrl(
792 | 'GET',
793 | bucket,
794 | object,
795 | expires: expires,
796 | reqParams: respHeaders,
797 | requestDate: requestDate,
798 | );
799 | }
800 |
801 | /// presignedPostPolicy can be used in situations where we want more control on the upload than what
802 | /// presignedPutObject() provides. i.e Using presignedPostPolicy we will be able to put policy restrictions
803 | /// on the object's `name` `bucket` `expiry` `Content-Type`
804 | Future presignedPostPolicy(PostPolicy postPolicy) async {
805 | if (_client.anonymous) {
806 | throw MinioAnonymousRequestError(
807 | 'Presigned POST policy cannot be generated for anonymous requests',
808 | );
809 | }
810 |
811 | final region = await getBucketRegion(postPolicy.formData['bucket']!);
812 | var date = DateTime.now().toUtc();
813 | var dateStr = makeDateLong(date);
814 |
815 | if (postPolicy.policy['expiration'] == null) {
816 | // 'expiration' is mandatory field for S3.
817 | // Set default expiration date of 7 days.
818 | var expires = DateTime.now().toUtc();
819 | expires.add(const Duration(days: 7));
820 | postPolicy.setExpires(expires);
821 | }
822 |
823 | postPolicy.policy['conditions'].add(['eq', r'$x-amz-date', dateStr]);
824 | postPolicy.formData['x-amz-date'] = dateStr;
825 |
826 | postPolicy.policy['conditions']
827 | .add(['eq', r'$x-amz-algorithm', 'AWS4-HMAC-SHA256']);
828 | postPolicy.formData['x-amz-algorithm'] = 'AWS4-HMAC-SHA256';
829 |
830 | postPolicy.policy['conditions'].add(
831 | ['eq', r'$x-amz-credential', '$accessKey/${getScope(region, date)}'],
832 | );
833 |
834 | postPolicy.formData['x-amz-credential'] =
835 | '$accessKey/${getScope(region, date)}';
836 |
837 | if (sessionToken != null) {
838 | postPolicy.policy['conditions']
839 | .add(['eq', r'$x-amz-security-token', sessionToken]);
840 | }
841 |
842 | final policyBase64 = jsonBase64(postPolicy.policy);
843 | postPolicy.formData['policy'] = policyBase64;
844 |
845 | final signature =
846 | postPresignSignatureV4(region, date, secretKey, policyBase64);
847 |
848 | postPolicy.formData['x-amz-signature'] = signature;
849 | final url = _client
850 | .getBaseRequest(
851 | 'POST',
852 | postPolicy.formData['bucket'],
853 | null,
854 | region,
855 | null,
856 | null,
857 | null,
858 | null,
859 | )
860 | .url;
861 | var portStr = (port == 80 || port == 443) ? '' : ':$port';
862 | var urlStr = '${url.scheme}://${url.host}$portStr${url.path}';
863 | return PostPolicyResult(postURL: urlStr, formData: postPolicy.formData);
864 | }
865 |
866 | /// Generate a presigned URL for PUT.
867 | /// Using this URL, the browser can upload to S3 only with the specified object name.
868 | ///
869 | /// - [bucketName]: name of the bucket
870 | /// - [objectName]: name of the object
871 | /// - [expires]: expiry in seconds (optional, default 7 days)
872 | Future presignedPutObject(
873 | String bucket,
874 | String object, {
875 | int? expires,
876 | }) {
877 | MinioInvalidBucketNameError.check(bucket);
878 | MinioInvalidObjectNameError.check(object);
879 | return presignedUrl('PUT', bucket, object, expires: expires);
880 | }
881 |
882 | /// Generate a generic presigned URL which can be
883 | /// used for HTTP methods GET, PUT, HEAD and DELETE
884 | ///
885 | /// - [method]: name of the HTTP method
886 | /// - [bucketName]: name of the bucket
887 | /// - [objectName]: name of the object
888 | /// - [expires]: expiry in seconds (optional, default 7 days)
889 | /// - [reqParams]: request parameters (optional)
890 | /// - [requestDate]: A date object, the url will be issued at (optional)
891 | Future presignedUrl(
892 | String method,
893 | String bucket,
894 | String object, {
895 | int? expires,
896 | String? resource,
897 | Map? reqParams,
898 | DateTime? requestDate,
899 | }) async {
900 | MinioInvalidBucketNameError.check(bucket);
901 | MinioInvalidObjectNameError.check(object);
902 |
903 | if (expires != null && expires < 0) {
904 | throw MinioInvalidArgumentError('invalid expire time value: $expires');
905 | }
906 |
907 | expires ??= expires = 24 * 60 * 60 * 7; // 7 days in seconds
908 | reqParams ??= {};
909 | requestDate ??= DateTime.now().toUtc();
910 |
911 | final region = await getBucketRegion(bucket);
912 | final request = _client.getBaseRequest(
913 | method,
914 | bucket,
915 | object,
916 | region,
917 | resource,
918 | reqParams,
919 | {},
920 | null,
921 | );
922 | return presignSignatureV4(this, request, region, requestDate, expires);
923 | }
924 |
925 | /// Uploads the object. Returns the ETag of the uploaded object.
926 | Future putObject(
927 | String bucket,
928 | String object,
929 | Stream data, {
930 | int? size,
931 | int? chunkSize,
932 | Map? metadata,
933 | void Function(int)? onProgress,
934 | }) async {
935 | MinioInvalidBucketNameError.check(bucket);
936 | MinioInvalidObjectNameError.check(object);
937 |
938 | if (size != null && size < 0) {
939 | throw MinioInvalidArgumentError('invalid size value: $size');
940 | }
941 |
942 | if (chunkSize != null && chunkSize < 5 * 1024 * 1024) {
943 | throw MinioInvalidArgumentError('Minimum chunk size is 5MB');
944 | }
945 |
946 | metadata = prependXAMZMeta(metadata ?? {});
947 |
948 | final partSize = chunkSize ?? _calculatePartSize(size ?? maxObjectSize);
949 |
950 | final uploader = MinioUploader(
951 | this,
952 | _client,
953 | bucket,
954 | object,
955 | partSize,
956 | metadata,
957 | onProgress,
958 | );
959 | final chunker = MinChunkSize(partSize);
960 | final etag = await data.transform(chunker).pipe(uploader);
961 | return etag.toString();
962 | }
963 |
964 | /// Remove all bucket notification
965 | Future removeAllBucketNotification(String bucket) async {
966 | await setBucketNotification(
967 | bucket,
968 | NotificationConfiguration(null, null, null),
969 | );
970 | }
971 |
972 | /// Remove a bucket.
973 | Future removeBucket(String bucket) async {
974 | MinioInvalidBucketNameError.check(bucket);
975 |
976 | final resp = await _client.request(
977 | method: 'DELETE',
978 | bucket: bucket,
979 | );
980 |
981 | validate(resp, expect: 204);
982 | _regionMap.remove(bucket);
983 | }
984 |
985 | /// Remove the partially uploaded object.
986 | Future removeIncompleteUpload(String bucket, String object) async {
987 | MinioInvalidBucketNameError.check(bucket);
988 | MinioInvalidObjectNameError.check(object);
989 |
990 | final uploadId = await findUploadId(bucket, object);
991 | if (uploadId == null) return;
992 |
993 | final resp = await _client.request(
994 | method: 'DELETE',
995 | bucket: bucket,
996 | object: object,
997 | queries: {'uploadId': uploadId},
998 | );
999 |
1000 | validate(resp, expect: 204);
1001 | }
1002 |
1003 | /// Remove the specified object.
1004 | Future removeObject(String bucket, String object) async {
1005 | MinioInvalidBucketNameError.check(bucket);
1006 | MinioInvalidObjectNameError.check(object);
1007 |
1008 | final resp = await _client.request(
1009 | method: 'DELETE',
1010 | bucket: bucket,
1011 | object: object,
1012 | );
1013 |
1014 | validate(resp, expect: 204);
1015 | }
1016 |
1017 | /// Remove all the objects residing in the objectsList.
1018 | Future removeObjects(String bucket, List objects) async {
1019 | MinioInvalidBucketNameError.check(bucket);
1020 |
1021 | final bunches = groupList(objects, 1000);
1022 |
1023 | for (var bunch in bunches) {
1024 | final payload = Delete(
1025 | bunch.map((key) => ObjectIdentifier(key, null)).toList(),
1026 | true,
1027 | ).toXml().toString();
1028 |
1029 | final headers = {'Content-MD5': md5Base64(payload)};
1030 |
1031 | await _client.request(
1032 | method: 'POST',
1033 | bucket: bucket,
1034 | resource: 'delete',
1035 | headers: headers,
1036 | payload: payload,
1037 | );
1038 | }
1039 | }
1040 |
1041 | // Remove all the notification configurations in the S3 provider
1042 | Future setBucketNotification(
1043 | String bucket,
1044 | NotificationConfiguration config,
1045 | ) async {
1046 | MinioInvalidBucketNameError.check(bucket);
1047 |
1048 | final resp = await _client.request(
1049 | method: 'PUT',
1050 | bucket: bucket,
1051 | resource: 'notification',
1052 | payload: config.toXml().toString(),
1053 | );
1054 |
1055 | validate(resp, expect: 200);
1056 | }
1057 |
1058 | /// Set the bucket policy on the specified bucket.
1059 | ///
1060 | /// [policy] is detailed [here](https://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies.html).
1061 | Future setBucketPolicy(
1062 | String bucket, [
1063 | Map? policy,
1064 | ]) async {
1065 | MinioInvalidBucketNameError.check(bucket);
1066 |
1067 | final method = policy != null ? 'PUT' : 'DELETE';
1068 | final payload = policy != null ? json.encode(policy) : '';
1069 |
1070 | final resp = await _client.request(
1071 | method: method,
1072 | bucket: bucket,
1073 | resource: 'policy',
1074 | payload: payload,
1075 | );
1076 |
1077 | validate(resp, expect: 204);
1078 | }
1079 |
1080 | Future setObjectACL(String bucket, String object, String policy) async {
1081 | MinioInvalidBucketNameError.check(bucket);
1082 | MinioInvalidObjectNameError.check(object);
1083 |
1084 | await _client.request(
1085 | method: 'PUT',
1086 | bucket: bucket,
1087 | object: object,
1088 | queries: {'acl': policy},
1089 | );
1090 | }
1091 |
1092 | Future getObjectACL(String bucket, String object) async {
1093 | MinioInvalidBucketNameError.check(bucket);
1094 | MinioInvalidObjectNameError.check(object);
1095 |
1096 | final resp = await _client.request(
1097 | method: 'GET',
1098 | bucket: bucket,
1099 | object: object,
1100 | queries: {'acl': ''},
1101 | );
1102 |
1103 | validate(resp, expect: 200);
1104 |
1105 | return AccessControlPolicy.fromXml(
1106 | xml.XmlDocument.parse(resp.body)
1107 | .findElements('AccessControlPolicy')
1108 | .first,
1109 | );
1110 | }
1111 |
1112 | /// Stat information of the object.
1113 | Future statObject(
1114 | String bucket,
1115 | String object, {
1116 | bool retrieveAcls = true,
1117 | }) async {
1118 | MinioInvalidBucketNameError.check(bucket);
1119 | MinioInvalidObjectNameError.check(object);
1120 |
1121 | final resp = await _client.request(
1122 | method: 'HEAD',
1123 | bucket: bucket,
1124 | object: object,
1125 | );
1126 |
1127 | validate(resp, expect: 200);
1128 |
1129 | var etag = resp.headers['etag'];
1130 | if (etag != null) {
1131 | etag = trimDoubleQuote(etag);
1132 | }
1133 |
1134 | return StatObjectResult(
1135 | etag: etag,
1136 | size: int.parse(resp.headers['content-length']!),
1137 | metaData: extractMetadata(resp.headers),
1138 | lastModified: parseRfc7231Time(resp.headers['last-modified']!),
1139 | acl: retrieveAcls ? await getObjectACL(bucket, object) : null,
1140 | );
1141 | }
1142 | }
1143 |
--------------------------------------------------------------------------------