zuraffa_minio 7.3.6
zuraffa_minio: ^7.3.6 copied to clipboard
A maintained fork of the minio package - a pure Dart client for MinIO and Amazon S3 compatible object storage.
zuraffa_minio
A maintained fork of the minio package — the
pure Dart client for MinIO and Amazon S3 compatible object storage.
Why a fork? #
The upstream minio package has been unmaintained since August 2025. Its
issue tracker grew to 36 open issues, most of them unanswered, and its test
suite pointed at a public demo server that no longer exists — so the suite was
red for everyone, not just for the maintainer.
zuraffa_minio keeps the API of the original package unchanged, so
migrating is an import swap:
// before
import 'package:minio/minio.dart';
// after
import 'package:zuraffa_minio/minio.dart';
// or, the default library, which is web safe:
import 'package:zuraffa_minio/zuraffa_minio.dart';
Nothing else in your code has to change: the class is still Minio, the
methods keep their names and their parameters, and the models keep their
fields.
The models live in their own library, exactly as they did upstream:
import 'package:zuraffa_minio/models.dart';
They are deliberately not exported from the default library: one of them is
called Object, and an export would shadow the Object of dart:core in
every file that imports the package.
What the fork adds on top:
| Dependencies | xml 7 (upstream was pinned to 6.4.2), and every other pin moved to its current release |
| Tests | A hermetic suite that runs against a real MinIO server in CI — the upstream suite could not run at all |
| Bug fixes | Six reported bugs fixed, plus four more found on the way (see What was fixed) |
| Features | The feature requests from the issue tracker: cancellation, credential renewal, presigned URL overrides, progress callbacks, parallel transfer, objectUrl, removeObjectsByPrefix |
Usage #
Upgrading from 0.2.4 #
Minio(...) requires accessKey and secretKey again. A 0.2.4 client that
omitted them no longer compiles, instead of signing every request with an empty
key and staying anonymous without saying so. A client that resolves its own
credentials calls Minio.withCredentialsProvider(...), which needs no
placeholder keys; once its provider resolves, those credentials take precedence
over any static keys the client was built with.
The constructor change shipped in a patch release (0.2.5), so ^0.2.4 resolves
it and a dart pub upgrade moves you onto it. Pin 0.2.4 to stay on the old
constructor, or depend on ^0.2.5 to adopt the new one. The next breaking
change ships as 0.3.0.
Initialize the client #
A local MinIO server
final minio = Minio(
endPoint: '127.0.0.1',
port: 9000,
accessKey: 'minioadmin',
secretKey: 'minioadmin',
useSSL: false,
);
Amazon S3
final minio = Minio(
endPoint: 's3.amazonaws.com',
accessKey: 'YOUR-ACCESSKEYID',
secretKey: 'YOUR-SECRETACCESSKEY',
region: 'eu-central-1',
);
Upload and download #
import 'dart:typed_data';
import 'package:zuraffa_minio/minio.dart';
Future<void> main() async {
final minio = Minio(
endPoint: '127.0.0.1',
port: 9000,
accessKey: 'minioadmin',
secretKey: 'minioadmin',
useSSL: false,
);
final bytes = Uint8List.fromList('hello world'.codeUnits);
final etag = await minio.putObject(
'my-bucket',
'greeting.txt',
Stream<Uint8List>.value(bytes),
size: bytes.length,
onProgress: (written) => print('uploaded $written bytes'),
);
print('etag: $etag');
final data = await minio.getObject('my-bucket', 'greeting.txt');
final buffer = BytesBuilder();
await for (final chunk in data) {
buffer.add(chunk);
}
print(String.fromCharCodes(buffer.takeBytes()));
minio.close();
}
Files #
package:zuraffa_minio/io.dart adds the dart:io helpers. It is a separate
library on purpose: the default library stays web safe.
import 'package:zuraffa_minio/io.dart';
await minio.fPutObject('my-bucket', 'picture.png', '/tmp/picture.png');
await minio.fGetObject('my-bucket', 'picture.png', '/tmp/picture-copy.png');
Presigned URLs #
// expires is a number of seconds
final url = await minio.presignedGetObject(
'my-bucket',
'greeting.txt',
expires: 3600,
);
When the server the client talks to is not the host the browser reaches — a proxy, a CDN, an internal and an external address — the presigned URL can be generated for the public host while the client keeps talking to the internal one:
final url = await minio.presignedUrl(
'GET',
'my-bucket',
'greeting.txt',
expires: 3600,
host: 'objects.example.com',
port: 443,
useSSL: true,
);
The override is used in the url and in the signature, so the two can never disagree.
Temporary credentials #
STS credentials expire. Instead of recreating the client, hand it a provider that returns fresh credentials when they are needed:
final minio = Minio.withCredentialsProvider(
endPoint: 's3.amazonaws.com',
region: 'eu-central-1',
credentialsProvider: () async {
final session = await assumeRoleWithWebIdentity();
return MinioCredentials(
accessKey: session.accessKey,
secretKey: session.secretKey,
sessionToken: session.sessionToken,
);
},
);
The provider is called before the first request is signed, and again whenever
the credentials it returned are older than credentialsTtl (five minutes by
default). refreshCredentials() forces a new resolution right away. The
accessKey, secretKey and sessionToken getters always report the
credentials the next request will be signed with.
Cancelling #
final token = MinioCancellationToken();
final minio = Minio(
endPoint: '127.0.0.1',
port: 9000,
accessKey: 'minioadmin',
secretKey: 'minioadmin',
useSSL: false,
cancellationToken: token,
);
final upload = minio.putObject(
'my-bucket',
'big.bin',
Stream<Uint8List>.value(data),
);
token.cancel(); // aborts the request that is in flight
await upload; // the upload fails
Cancelling closes the underlying HTTP client, so the request that is in flight
is aborted. A request that has not been sent yet fails with a
MinioRequestCancelledError.
minio.close() aborts everything that is running as well.
Your own HTTP client #
The client is injectable, which is the escape hatch for proxies, custom certificates and retry policies:
import 'package:http/io_client.dart';
final minio = Minio(
endPoint: 'example.com',
accessKey: '...',
secretKey: '...',
httpClient: IOClient(HttpClient()..badCertificateCallback = (_, __, ___) => true),
);
package:zuraffa_minio/io.dart has a shortcut for the certificate case:
final minio = Minio(
endPoint: 'example.com',
accessKey: '...',
secretKey: '...',
httpClient: createTlsHttpClient(allowInvalidCertificates: true),
);
Parallel transfers #
// upload the parts of a multipart upload concurrently
await minio.putObject(
'my-bucket',
'big.bin',
Stream<Uint8List>.value(data),
size: data.length,
maxParallelUploads: 4,
);
// download the object as concurrent byte ranges
final stream = await minio.getObject(
'my-bucket',
'big.bin',
parallelism: 8,
chunkSize: 4 << 20,
);
Regions #
The region decides two things: the AWS host the request goes to and the scope the request is signed with. The client keeps both in sync, and it asks the server for the region of a bucket when it does not know it yet.
If the server rejects a request because the region is wrong, it answers with
AuthorizationHeaderMalformed and names the region the bucket really lives
in. The client then signs and addresses the request again with that region
and remembers it for the bucket.
What was fixed #
Every fix has a regression test whose name carries the number of the upstream issue it reproduces.
| Issue | What was wrong |
|---|---|
| #6 | The notification poller called json.decode on the XML payload MinIO sends |
| #24 | A negative object size failed an assert, which only exists in debug builds |
| #38 | An unknown region fell back to the global endpoint, which only accepts us-east-1 signatures |
| #43 | getRequestUrl read the region without a null check and threw "Unexpected null value" |
| #49 | listBuckets cast the whitespace text nodes between the buckets to XmlElement |
| #108 | statObject read content-length and last-modified with a bang operator |
Four more bugs were found while writing those tests:
listIncompleteUploadssent theuploadsparameter twice, which broke the signature of every request it made.listPartsandlistIncompleteUploadsread the wrong XML elements and therefore always reported an empty, untruncated page.listPartscast the document node toXmlElement, which is aTypeError.listIncompleteUploadsfailed as a whole when a single upload could not be inspected any more.
Running the tests #
The suite needs an S3 compatible server. By default it talks to
127.0.0.1:9000 with the minioadmin credentials:
minio server /tmp/minio-data --address 127.0.0.1:9000
dart test -j 2
Without a server the integration tests are skipped and the offline tests still run. The server can be pointed somewhere else:
| Variable | Default | Meaning |
|---|---|---|
MINIO_ENDPOINT |
127.0.0.1 |
The host of the server |
MINIO_PORT |
9000 |
The port of the server |
MINIO_USE_SSL |
false |
Whether the server speaks TLS |
MINIO_ACCESS_KEY |
minioadmin |
The access key |
MINIO_SECRET_KEY |
minioadmin |
The secret key |
MINIO_REGION |
us-east-1 |
The region to sign for |
MinIO's open source server is archived: the docker images are gone and
dl.min.ioanswers410 Gone. The CI workflow downloads the last release that ships a binary and runs that.
API #
| Bucket operations | Object operations | Presigned operations | Bucket policy & notification |
|---|---|---|---|
makeBucket |
getObject |
presignedUrl |
getBucketNotification |
listBuckets |
getPartialObject |
presignedGetObject |
setBucketNotification |
bucketExists |
putObject |
presignedPutObject |
removeAllBucketNotification |
removeBucket |
statObject |
presignedPostPolicy |
listenBucketNotification |
listObjects |
copyObject |
getBucketPolicy |
|
listObjectsV2 |
removeObject |
setBucketPolicy |
|
listIncompleteUploads |
removeObjects |
||
listAllObjects |
removeObjectsByPrefix |
||
listAllObjectsV2 |
objectUrl |
||
initiateNewMultipartUpload |
|||
uploadPart |
|||
completeMultipartUpload |
|||
removeIncompleteUpload |
What was added #
Every addition answers an open issue of the upstream package, so the issue tracker and the API stay traceable:
| Issue | What was added |
|---|---|
| #4, #20 | onProgress on getObject and getPartialObject |
| #12 | objectUrl |
| #35, #98 | putObject(maxParallelUploads:) |
| #45, #85, #93 | MinioCancellationToken and MinioRequestCancelledError |
| #80 | createTlsHttpClient |
| #94 | MinioCredentials, credentialsProvider, refreshCredentials |
| #99 | removeObjectsByPrefix |
| #101 | host, port and useSSL on the presigned calls |
| #102 | uploadPart and the multipart example |
| #112 | getObject(parallelism:) |
License #
MIT — see LICENSE. The original package is MIT licensed as well;
the copyright of xuty is kept.