本文主要叙述在 Flutter 项目中如何完成将文件上传到华为 OBS(目标存储)中,并封装为三方库方便灵敏运用。

布景介绍

在大多项目中都会存在文件上传的需求,之前的完成都是调用后台的文件上传接口将文件上传到服务器上,可是这样会存在一个问题,由于文件上传会占用带宽导致在文件上传中调用其他接口的时分就会存在拜访慢的情况,解决计划当然是升级带宽或者单独运用一台服务器作为文件服务,并且要带宽满意大否则上传下载的时分会很慢,可是这样两种计划本钱都比较高。随着云计算的到来,各大云服务商都供给了目标存储的服务,费用廉价、带宽高、不影响事务系统并且供给了许多附加功用,比方图片处理、图片鉴黄等功用。

因现在在做的项目甲方爸爸明确要求云服务要运用华为云,所以目标存储服务也有必要运用华为云的 OBS 服务,而为了节省人力本钱移动端运用的是 Flutter 跨渠道开发,所以就有了本篇文章标题的需求,需求在 Flutter 中完成将文件上传到华为云 OBS 中,而华为云 OBS 并没有供给 Flutter SDK,所以就需求自己完成,首要看一下完成今后的代码运用效果。

运用

现在只封装了两个简单的功用:上传目标、上传文件。

首要在项目的 pubspec.yaml 里增加依赖,如下:

 flutter_hw_obs:
  git:
   url: https://github.com/loongwind/flutter_hw_obs.git
   ref: 0.0.3

然后在运用的地方引进obs_client包:

import 'package:flutter_hw_obs/obs_client.dart';

初始化

调用 OBSClient.init 进行初始化。

OBSClient.init("${AccessKey}", "${SecretAccessKey}", "${AccessDomain}", "${BucketName}");
​

参数阐明:

  • AccessKey: 用于标识华为用户,在华为云控制台创立子账号获取
  • SecretAccessKey: 用于验证用户的密钥,在华为云控制台创立子账号获取
  • AccessDomain: 拜访域名,创立 OBS 桶后会主动分配拜访域名,如xxx.obs.cn-southwest-2.myhuaweicloud.com
  • BucketName: 桶称号,创立 OBS 桶时的称号

在运用其他 api 之前有必要先进行初始化。

上传目标

运用 OBSClient.putObject 上传目标。

OBSResponse response = await OBSClient.putObject("${ObjectName}", data, xObsAcl="$xObsAcl");
​
OBSResponse response = await OBSClient.putObject("test/hello.txt", utf8.encode("Hello OBS"));

参数阐明:

  • ObjectName:目标称号,即存储到 OBS 上的文件称号,带途径,如:test/hello.txt
  • data: 上传目标数据,类型是 List<int> 的二进制数据
  • xObsAcl: 上传目标的权限控制控制战略,可选值如下表所示,默认为public-read 即公共读
预界说的权限控制战略 描述
private 桶或目标的所有者具有彻底控制的权限,其他任何人都没有拜访权限
public-read 设在桶上,所有人能够获取该桶内目标列表、桶内多段使命、桶的元数据、桶的多版别。设在目标上,所有人能够获取该目标内容和元数据。
public-read-write 设在桶上,所有人能够获取该桶内目标列表、桶内多段使命、桶的元数据、桶的多版别、上传目标删去目标、初始化段使命、上传段、合并段、复制段、撤销多段上传使命。设在目标上,所有人能够获取该目标内容和元数据。
public-read-delivered 设在桶上,所有人能够获取该桶内目标列表、桶内多段使命、桶的元数据、桶的多版别,能够获取该桶内目标的内容和元数据。不能应用在目标上。
public-read-write-delivered 设在桶上,所有人能够获取该桶内目标列表、桶内多段使命、桶的元数据、桶的多版别、上传目标删去目标、初始化段使命、上传段、合并段、复制段、撤销多段上传使命,能够获取该桶内目标的内容和元数据。不能应用在目标上。
bucket-owner-full-control 设在目标上,桶或目标的所有者具有彻底控制的权限,其他任何人都没有拜访权限。

回来成果是一个 OBSResponse 目标,代码如下:

class OBSResponse{
 String? objectName;
 String? fileName;
 String? url;
 int? size;
 String? ext;
 String? md5;
}

字段阐明:

objectName: 目标称号,即上传到 OBS 的途径

fileName: 文件称号

url: OBS 的拜访途径

size: 目标巨细

ext: 文件后缀

md5: 目标 MD5 值

上传文件

运用OBSClient.putFile 能够进行文件上传,代码如下:

OBSResponse response = await OBSClient.putFile("test/test.png", File("/sdcard/test.png"), xObsAcl="public-read");

该办法与 OBSClient.putObject 很像,榜首、第三个参数都相同,只有第二个参数不相同,这儿第二个参数是一个 File 目标。回来成果同样也是 OBSResponse 目标。

代码完成

华为 OBS 虽然没供给 Flutter 的 SDK,可是却供给了 Android 和 iOS 的 SDK,所以最开端想到的是写一个 Flutter 的插件分别集成 OBS 的 Android SDK 和 iOS SDK,也的确这么做了 Android SDK 很轻松的就集成完成了,可是集成 iOS SDK 的时分却遇到各种过错,最终无法放弃,当然也由于本人之前一直从事 Android 开发 iOS 开发能力不足导致。最终看了一下 OBS 的文档,有供给 API 的方式,而项目中的需求其实很简单便是上传文件,于是就用 Dart 结合 dio 完成了一个纯 Dart 的库。

创立 OBSResponse

首要创立一个 OBSResponse 实体类,用于上传 OBS 后的回来成果,代码如下:

class OBSResponse{
 String? objectName;
 String? fileName;
 String? url;
 int? size;
 String? ext;
 String? md5;
}

详细字段阐明在上面运用介绍里已经阐明晰,这儿就不过多介绍了。

创立 OBSClient

核心代码都在 OBSClient 里。首要界说 init 初始化办法,由于运用 OBS 的 API 需求一些有必要的认证参数,如下:

class OBSClient {
​
 static String? ak;
 static String? sk;
 static String? bucketName;
 static String? domain;
​
 static void init(String ak, String sk, String domain, String bucketName){
  OBSClient.ak = ak;
  OBSClient.sk = sk;
  OBSClient.domain = domain;
  OBSClient.bucketName = bucketName;
  }
}

然后界说初始化 dio 的办法,由于完成 api 恳求运用的是 dio,如下:

 static Dio _getDio() {
  var dio = Dio();
  dio.interceptors.add(PrettyDioLogger(
    requestHeader: true, requestBody: true, responseHeader: true));
  return dio;
  }

这儿很简单,便是初始化一个 Dio 目标,然后增加日志拦截器用于输出日志。

创立一个公共的 put 办法,由于 OBS 上传目标是一个一致的 api ,所以这儿也封装一个一致的上传目标办法,如下:

static Future<OBSResponse?> put(String objectName, data , String md5, int size, {String xObsAcl = "public-read"}) async{
  if(objectName.startsWith("/")){
   objectName = objectName.substring(1);
   }
  String url = "$domain/$objectName";
​
  var contentMD5 = md5;
  var date = HttpDate.format(DateTime.now());
  var contentType = "application/octet-stream";
​
  Map<String, String> headers = {};
  headers["Content-MD5"] = contentMD5;
  headers["Date"] = date;
  headers["x-obs-acl"] = xObsAcl;
  headers["Authorization"] = _sign("PUT", contentMD5, contentType, date, "x-obs-acl:$xObsAcl", "/$bucketName/$objectName");
​
  Options options = Options(headers: headers, contentType: contentType);
​
  Dio dio = _getDio();
​
  await dio.put(url, data: data, options: options);
  OBSResponse obsResponse = OBSResponse();
  obsResponse.md5 = contentMD5;
  obsResponse.objectName = objectName;
  obsResponse.url = url;
  obsResponse.fileName = path.basename(objectName);
  obsResponse.ext = path.extension(objectName);
  obsResponse.size = size;
  return obsResponse;
  }

该办法参数有 5 个, objectName 是存储到 OBS 的文件全途径,data 是上传目标的数据,md5 是 data 的 md5 值,size 是 data 的巨细,xObsAcl 是权限控制战略。其中 data 是一个动态类型,能够传入二进制数据、文件、字符串等,对应的获取 md5 和 size 的办法都不相同,所以这儿提取成了参数。

在办法完成里首要判断了 objectName 是否以 / 开端,由于 OBS 的途径不支持 / 开端,所以这儿做了处理,如果是 / 开端则移除 /

根据拜访域名 domainobjectName 拼装成 OBS 的拜访 url。

接下来拼装恳求的 Header,Content-MD5 即为上传目标的 MD5 值,Date 为当时时刻,x-obs-acl 便是传入的权限拜访战略,Authorization 是身份认证,需求对恳求进行签名,所以这儿封装了一个 _sign 签名办法,完成如下:

 static String _sign(String httpMethod, String contentMd5, String contentType,
   String date, String acl, String res) {
  if (ak == null || sk == null) {
   throw "ak or sk is null";
   }
  String signContent =
    "$httpMethod\n$contentMd5\n$contentType\n$date\n$acl\n$res";
​
  return "OBS $ak:${signContent.toHmacSha1Base64(sk!)}";
  }

签名的算法是先将恳求办法(PUT)、md5(目标 md5 值)、Content-Type(内容类型 application/octet-stream)、date(当时时刻)、acl(权限战略)、res(桶称号+objectName)拼装成一个字符串,然后对这个字符串进行 Hmac 编码再转 Base64,再在签名的内容前面拼上OBS 字符串和 AccessKey 值。toHmacSha1Base64 办法是自界说的字符串扩展办法,完成如下:

 String toHmacSha1Base64(String sk){
  var hmacSha1 = Hmac(sha1, utf8.encode(sk));
  return base64.encode(hmacSha1.convert(utf8.encode(this)).bytes);
  }

恳求头封装好后调用 dio 的 put 办法进行上传,上传成功后拼装 OBSResponse 进行回来。

这样通用的目标上传办法就完成了,接下看看 putObjectputFile 的完成:

 static Future<OBSResponse?> putObject(String objectName, List<int> data,{String xObsAcl = "public-read"}) async{
  String contentMD5 = data.toMD5Base64();
  int size = data.length;
  var stream = Stream.fromIterable(data.map((e) => [e]));
  OBSResponse? obsResponse = await put(objectName, stream, contentMD5, size, xObsAcl: xObsAcl);
  return obsResponse;
  }
​
 static Future<OBSResponse?> putFile(String objectName, File file,{String xObsAcl = "public-read"}) async{
  var contentMD5 = await getFileMd5Base64(file);
  var stream = file.openRead();
  OBSResponse? obsResponse = await put(objectName, stream, contentMD5, await file.length() xObsAcl: xObsAcl);
  return obsResponse;
  }

都是调用的上面封装的 put 办法,仅仅获取 md5 的办法、获取 size 的办法以及 data 不相同。

这儿分别对 List<int> 和文件的获取 md5 进行了封装,如下:

List:

extension ListIntExt on List<int>{
 List<int> toMD5Bytes(){
  return md5.convert(this).bytes;
  }
​
 String toMD5(){
  return toMD5Bytes().toString();
  }
​
 String toMD5Base64(){
  return base64.encode(toMD5Bytes());
  }
}

文件

Future<List<int>> getFileMd5Bytes(File file) async{
 var digest = await md5.bind(file.openRead()).first;
 return digest.bytes;
}
​
Future<String> getFileMd5Base64(File file) async{
 var md5bytes = await getFileMd5Bytes(file);
 return base64.encode(md5bytes);
}

最终 List<int> 和文件转化为 Stream 的办法也不相同,List<int> 是经过 Stream.fromIterable(data.map((e) => [e])); 转化,而文件是经过 file.openRead() 获取。

OK,大功告成,运用 Dart 经过 OBS api 完成目标上传的封装就完成了,虽然功用还不彻底,可是已经能满意最根底的运用了,希望对你有所协助,后续将对这个库进行继续完善以支持更多的功用。

源码地址:flutter_hw_obs

\