前言


在移动开发中,我们常常会遇到需要在App中处理文件上传和下载的需求。Minio是一个开源的对象存储服务,它兼容Amazon S3云存储服务接口,可以用于存储大规模非结构化的数据。

开始之前


在pubspec.yaml文件中添加minio_new库的依赖:

dependencies:minio_new: ^1.0.2

运行flutter pub get命令来获取依赖。可去pub上看 minio_new 最新版本。

初始化Minio客户端


需要先创建一个Minio客户端的实例。这个实例需要配置Minio服务器的连接信息,包括服务器的URL、端口号、访问密钥和密钥等。

var minio = Minio(endPoint: 'your-minio-server.com',port: 9000,useSSL: false,accessKey: 'your-access-key',secretKey: 'your-secret-key',);

参数介绍:
useSSL:指定是否使用 SSL 连接。如果设置为 true,则使用 HTTPS 协议进行连接;如果设置为 false,则使用 HTTP 协议。
endPoint:指定 MinIO 服务器的终端节点(Endpoint)。这是 MinIO 服务器的主机名或 IP 地址。
port:指定连接 MinIO 服务器的端口号。
accessKey:指定用于身份验证的 MinIO 服务器的访问密钥。这是访问 MinIO 存储桶和对象所需的身份验证凭据之一,就是账号。
secretKey:指定用于身份验证的 MinIO 服务器的秘密密钥。与访问密钥一同用于身份验证,就是密码。

创建桶(Bucket)


在Minio中,桶(Bucket)是一种用于组织和存储对象的容器。类似于文件系统中的文件夹,桶在Minio中用于对对象进行逻辑分组和管理。每个桶都具有唯一的名称,并且可以在Minio服务器上创建多个桶。

桶的命名规则:只能包含小写字母、数字和连字符(-),并且长度必须在3到63个字符之间。桶的名称在Minio服务器上必须是唯一的。

 Future<void> createBucket(String bucketName) {minio.makeBucket(bucketName);//设置桶的公用权限,这样外界才能通过链接访问return minio.setBucketPolicy(bucketName, {"Version": "2012-10-17","Statement": [{"Sid": "PublicRead",//一个可选参数,表示这个策略的 ID,可以随意填写。"Effect": "Allow",//表示策略的效果,如果希望所有人都可以读取,那么这里就填写 'Allow'。"Principal": "*",//表示策略的主体,如果希望所有人都可以读取,那么这里就填写 '*'。"Action": ["s3:GetObject"],//一个数组,表示允许的操作,如果希望所有人都可以读取,那么就填写 ['s3:GetObject']。"Resource": ["arn:aws:s3:::$bucketName/*"]//一个数组,表示策略的资源,如果希望所有人都可以读取桶中的所有对象,那么就填写 ['arn:aws:s3:::your_bucket/*']。}]});}

因为无论是上传还是下载文件都是基于桶进行操作的,所以初始化之后,在上传文件之前需要先创建桶,可以通过minio.bucketExists事先来判断桶是否存在。

如果不设置桶的权限的话,也就是不调用上面minio.setBucketPolicy方法,默认创建的桶是私有的,外界不能通过链接访问相关文件,出了调用minio.setBucketPolicy设置权限外,也可以在Minio后台设置桶的权限,如下图:

上传文件


 ///上传文件Future<String> uploadFile(String filename, String filePath) async {minio.fPutObject(bucketName, filename, filePath);//返回上传文件的完整访问路径return getUrl(filename);}

bucketName:要上传到哪个桶就写哪个桶名。

filename: 文件名,如:a.png。

filePath: 要上传文件的路径。

下载文件同理。

完整代码


minio.dart

import 'dart:async';import 'dart:io';import 'package:ecology/utils/log_util.dart';import 'package:ecology/utils/toast.dart';import 'package:minio_new/io.dart';import 'package:minio_new/minio.dart';import 'package:minio_new/models.dart';import 'package:path/path.dart' show dirname;import 'package:path_provider/path_provider.dart';// ignore: unused_importimport 'package:rxdart/rxdart.dart';class Prefix {bool isPrefix;String key;String prefix; //使用前缀可以帮助你更好地组织和管理对象,避免冲突和重复,并方便批量操作,不使用传''Prefix({required this.key, required this.prefix, required this.isPrefix});}var _minio;Future<Minio> _resetMinio() async {//固定配置-换成你实际的bool useSSl = false;String endPoint = 'red.xxx.com';int port = 9000;String accessKey = 'xxx';String secretKey = 'xxx';try {_minio = Minio(useSSL: useSSl,endPoint: endPoint,port: port,accessKey: accessKey,secretKey: secretKey,region: 'cn-north-1',);} catch (err) {XToast.show(err.toString());return Future.error(err);}return _minio;}class MinioController {late Minio minio;String bucketName;String prefix;static resetMinio() async {await _resetMinio();}/// maximum object size (5TB)final maxObjectSize = 5 * 1024 * 1024 * 1024 * 1024;///传入唯一桶名,自动初始化桶MinioController({required this.bucketName,this.prefix = ''}) {if (_minio is Minio) {minio = _minio;//初始化桶-由已有用户切换为新用户的情况下buckerExists(bucketName).then((exists) {if(!exists) {createBucket(bucketName);}});} else {_resetMinio().then((_) {minio = _;//初始化桶buckerExists(bucketName).then((exists) {if(!exists) {createBucket(bucketName);}});});}}///用于列出存储桶中未完成的分块上传任务。这个函数允许你获取所有处于未完成状态的分块上传任务的信息,以便你可以对其进行管理或继续上传。Future<List<IncompleteUpload>> listIncompleteUploads({String" />}) async {final list =minio.listIncompleteUploads(bucketName ?? this.bucketName, '').toList();return list;}///获取桶对象///用于获取指定桶中的对象列表,并返回一个包含前缀列表和对象列表的MapFuture<Map<dynamic, dynamic>> getBucketObjects(String prefix) async {//listObjectsV2:列出指定桶中的对象。它返回一个 Stream 对象,该对象会按需逐个返回对象信息。final objects =minio.listObjectsV2(bucketName, prefix: prefix, recursive: false);final map = {};await for (var obj in objects) {final prefixs = obj.prefixes.map((e) {final index = e.lastIndexOf('/') + 1;final prefix = e.substring(0, index);final key = e;return Prefix(key: key, prefix: prefix, isPrefix: true);}).toList();map['prefixes'] = prefixs;map['objests'] = obj.objects;}return map;}///获取桶列表Future<List<Bucket>> getListBuckets() async {return minio.listBuckets();}///桶是否存在Future<bool> buckerExists(String bucket) async {return minio.bucketExists(bucket);}///下载文件Future<void> downloadFile(filename) async {final dir = await getExternalStorageDirectory();minio.fGetObject(bucketName, prefix + filename, '${dir?.path}/${prefix + filename}').then((value) {});}///上传文件Future<String> uploadFile(String filename, String filePath) async {minio.fPutObject(bucketName, filename, filePath);//返回上传文件的完整访问路径return getUrl(filename);}///批量上传文件Future<void> uploadFiles(List<String> filepaths, String bucketName) async {for (String filepath in filepaths) {String filename = filepath.split('/').last;await minio.fPutObject(bucketName, filename, filepath,);}}String getUrl(String filename) {return 'http://${minio.endPoint}:${minio.port}/$bucketName/$filename';}///用于生成一个预签名的 URL,该 URL 允许在一定时间内以有限的权限直接访问 MinIO 存储桶中的对象Future<String> presignedGetObject(String filename, {int? expires}) {return minio.presignedGetObject(bucketName, filename, expires: expires);}///获取一个文件一天的访问链接Future<String> getPreviewUrl(String filename) {return presignedGetObject(filename, expires: 60 * 60 * 24);}/// 可多删除和单删除Future<void> removeFiles(List<String> filenames) {return minio.removeObjects(bucketName, filenames);}///创建桶Future<void> createBucket(String bucketName) {minio.makeBucket(bucketName);//设置桶的公用权限,这样外界才能通过链接访问return minio.setBucketPolicy(bucketName, {"Version": "2012-10-17","Statement": [{"Sid": "PublicRead",//一个可选参数,表示这个策略的 ID,可以随意填写。"Effect": "Allow",//表示策略的效果,如果希望所有人都可以读取,那么这里就填写 'Allow'。"Principal": "*",//表示策略的主体,如果希望所有人都可以读取,那么这里就填写 '*'。"Action": ["s3:GetObject"],//一个数组,表示允许的操作,如果希望所有人都可以读取,那么就填写 ['s3:GetObject']。"Resource": ["arn:aws:s3:::$bucketName/*"]//一个数组,表示策略的资源,如果希望所有人都可以读取桶中的所有对象,那么就填写 ['arn:aws:s3:::your_bucket/*']。}]});}///移除桶Future<void> removeBucket(String bucketName) {return minio.removeBucket(bucketName);}///用于获取 MinIO 存储桶中对象的部分内容,即获取对象的部分数据。这个函数可以用于实现断点续传、分片下载或其他需要获取对象部分内容的场景。Future<dynamic> getPartialObject(String bucketName, String filename, String filePath,{required void Function(int downloadSize, int? fileSize) onListen,required void Function(int downloadSize, int? fileSize) onCompleted,required void Function(StreamSubscription<List<int>> subscription)onStart}) async {final stat = await this.minio.statObject(bucketName, filename);final dir = dirname(filePath);await Directory(dir).create(recursive: true);final partFileName = '$filePath.${stat.etag}.part.minio';final partFile = File(partFileName);IOSink partFileStream;var offset = 0;final rename = () => partFile.rename(filePath);if (await partFile.exists()) {final localStat = await partFile.stat();if (stat.size == localStat.size) return rename();offset = localStat.size;partFileStream = partFile.openWrite(mode: FileMode.append);} else {partFileStream = partFile.openWrite(mode: FileMode.write);}final dataStream =(await minio.getPartialObject(bucketName, filename, offset)).asBroadcastStream(onListen: (sub) {if (onStart != null) {onStart(sub);}});Future.delayed(Duration.zero).then((_) {final listen = dataStream.listen((data) {if (onListen != null) {onListen(partFile.statSync().size, stat.size);}});listen.onDone(() {if (onListen != null) {onListen(partFile.statSync().size, stat.size);}listen.cancel();});});await dataStream.pipe(partFileStream);if (onCompleted != null) {onCompleted(partFile.statSync().size, stat.size);}final localStat = await partFile.stat();if (localStat.size != stat.size) {throw MinioError('Size mismatch between downloaded file and the object');}return rename();}}