前言


在移动开发中,我们常常会遇到需要在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();
  }
}

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐