前言
在移动开发中,我们常常会遇到需要在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_import
import '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? bucketName}) async {
final list =
minio.listIncompleteUploads(bucketName ?? this.bucketName, '').toList();
return list;
}
///获取桶对象
///用于获取指定桶中的对象列表,并返回一个包含前缀列表和对象列表的Map
Future<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();
}
}