Flutter实现文件上传华为对象存储(OBS)

news2025/1/12 8:48:17

    本文主要讲述在 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"));

【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

  

参数说明:

  • 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

作者:loongwind  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/76222.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

从投资人发现“新大陆”,看“产融星城”为何成?

文丨熔财经 作者|陈小江 “我从来不把《我要投资》节目看作是秀&#xff08;综艺&#xff09;&#xff0c;要知道《我要投资》是在前面的&#xff0c;秀&#xff08;综艺&#xff09;只是一种形式而已。本质上&#xff0c;&#xff08;参加节目&#xff09;还是为了投资”。 …

「Redis数据结构」跳跃表(SkipList)

「Redis数据结构」跳跃表&#xff08;SkipList&#xff09; 文章目录「Redis数据结构」跳跃表&#xff08;SkipList&#xff09;一、概述二、结构跳跃表节点跳跃表三、特点一、概述 跳表&#xff08;SkipList&#xff0c;全称跳跃表&#xff09;是用于有序元素序列快速搜索查找…

shell编程二

目录语法引号exprtestif...then...fiif...elseif...elseif嵌套case…esacforwhilebreak 和 continue函数语法 引号 " ":如果有字符串&#xff0c;字符串原样输出&#xff0c;如果有$变量则查看变量的值 :所有的字符无论是否有变量都原样输出 ˋˋ:在该引号包含的…

如何批量查询网站的搜狗收录情况?搜狗收录么查询

如何批量查询网站的搜狗收录情况&#xff1f;搜狗收录么查询 查询网站的搜狗收录的具体操作&#xff1a; 第一步、打开网站综合查询工具 第二步、添加需要查询的网站域名 第三步、勾选要查询的功能&#xff08;勾选搜狗是否收录和搜狗总收录&#xff09; 第…

redis 主从复制(读写分离)集群搭建(含错误处理)

目录 1.概述 2.搭建 2.1.安装配置 2.2.认主 2.3.错误处理 3.原理 1.概述 当面临大流量时&#xff0c;redis可以采取集群的方式进行扩容&#xff0c;将压力分散到集群中的多个结点上去防止redis被打挂。redis的扩容方式有两种&#xff1a; 垂直扩容&#xff0c;即读写分离…

【移动端测试】了解Android的配置和使用过程

Android 是基于JAVA语言来进行开发编写的&#xff0c;但是对于Android体系中最底层是Linux层&#xff0c;现在说一下android 项目的结构目录: 整体结构和一个普通的java项目很类似 每一个项目都有一个主Activity 相当于java类中的main 方法是程序的入口 比如 该项目中的ListVi…

centos7搭建nginx主从以及集群

一、nginx升级之路 之前因为业务量并不是很大&#xff0c;所以公司nginx采用的是单机。因为nginx单机性能也很好&#xff0c;所以也没有发生过什么问题。不过后来还是慢慢进行了几次调整。最终换成了多IP地址解析和nginx主从。下面就介绍一下怎么一步步升级的。 1.最初版本&a…

天翼物联携手6家单位发起移动物联网高质量发展共同倡议

近日&#xff0c;由工信部指导&#xff0c;中国信息通信研究院&#xff08;以下简称“中国信通院”&#xff09;、中国通信学会、无锡市人民政府、人民邮电报社、江苏省工业和信息化厅、江苏省通信管理局共同主办&#xff0c;无锡物联网创新促进中心、天翼物联科技有限公司等单…

uniapp中tabBar菜单栏的实现以及页面常用的生命周期(菜单栏颜色切换)

前言 本篇文章带大家使用uniapp通过小案例实现tab菜单栏的切换&#xff0c;并对页面中常用的生命周期进行介绍。 实现菜单栏的切换 配置page页面 我们这里要实现三个页面的切换&#xff0c;所以要先在page.json文件中配置三个页面的路径 "pages": [ //pages数组…

HTML -- 一文学会HTML及常用标签

文章目录1. HTML简介1.1 网页1.1.1 什么是网页1.1.2 什么是HTML1.1.3 网页的形成1.2 常用浏览器1.2.1 常见的浏览器1.2.2 浏览器内核1.3 Web标准1.3.1 为什么需要Web标准1.3.2 Web标准的构成2. HTML基础2.1 HTML语法规范2.1.1 基本语法规范2.1.2 标签关系2.2 HTML基本结构标签2…

我做软件测试工作的两大乐趣

大家好&#xff0c;我是小谭。 曾几何时&#xff0c;我们都有一个梦想&#xff0c;梦想着做自己喜欢的工作。但现实往往事与愿违&#xff0c;我们被家庭、身份、社会捆绑&#xff0c;做着自己不喜欢的工作&#xff0c;即便做着自己喜欢的工作&#xff0c;也容易在日常的琐碎中…

SAAS系统和ERP区别?

saas系统和erp区别&#xff1f; saas是云计算应用的一种形式,而传统erp系统并不具备此特征&#xff1b;saas系统的数据存储也是在云端上&#xff0c;只要使用的软件即可用于业务管理的属于ERP的类别。 saas行业和传统软件区别很大吗&#xff1f; 很大&#xff0c;一个是新兴产…

C#捐款信息管理系统

捐款信息管理系统 技术 C#sqlserver 主要功能 登录、注册功能&#xff0c;两种角色登录判断&#xff0c;一种为管理员&#xff0c;一种为用户管理员发起项目&#xff0c;即发布项目&#xff0c;同时拥有对项目的增加、删除、修改、查询用户对管理员发起的项目可以进行申请操…

魏副业而战:做闲鱼副业项目的3个阶段

我是魏哥&#xff0c;与其在家躺平&#xff0c;不如魏副业而战&#xff01; 小伙在社群提问&#xff1a;有没整套的模板案例&#xff1f; 他找了一上午的同行&#xff0c;没有找到“完整”的案例&#xff0c;他很苦恼&#xff0c;所以想请老师指导一下。 其实呢&#xff0c;…

wait_queue如何使用

Linux内核的 等待队列&#xff08;Wait Queue&#xff09;是重要的数据结构&#xff0c;与进程调度机制紧密相关联&#xff0c;可以用来同步对系统资源的访问、异步事件通知、跨进程通信等。如下图所示&#xff0c; 在Linux中&#xff0c;等待队列以循环链表为基础结构&#xf…

基于web的实验教学管理系统java ssm教学视频平台源码和论文

研究背景 近几年来&#xff0c;随着地方高等院校办学规模的不断扩大&#xff0c;为了适用社会发展需要&#xff0c;地方高校将应用 型人才培养作为学校的人才培养目标。为了适应学校应用型人才培养目标&#xff0c;各专业尤其是理工科专 业人才培养方案中加强了实践教学环节&a…

1559_AURIX_TC275_RCU系统中的Boot支持、Pad配置以及NMI trap处理

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 这一次看的10页文档虽然文字内容不多&#xff0c;但是涉及到的面还是很多的。而且&#xff0c;看完之后的确是有一种答疑解惑的畅快感。 1. ESRx作为GPIO用的时候&#xff0c;也拥有GPIO设…

高等数学(第七版)同济大学 习题11-1 个人解答

高等数学&#xff08;第七版&#xff09;同济大学 习题11-1 函数作图软件&#xff1a;Mathematica 1.设在xOy面内有一分布着质量的曲线弧L&#xff0c;在点(x,y)处它的线密度为μ(x,y)&#xff0c;用对弧长的曲线积分分别表达&#xff1a;\begin{aligned}&1. \ 设在xOy面内…

蓝桥杯嵌入式MCP4017

文章目录前言一、查看MCP4017芯片手册二、MCP4017在开发板上的电路图三、工程配置四、MCP4017读写函数编写总结前言 MCP4017是一个可编程电阻&#xff0c;通过写入数值可以调节电阻值的大小。 一、查看MCP4017芯片手册 在这里我们只需要关注MCP4017即可。下面的几个重要点我…

广播、组播 socket编程

目录 1、单播 / 广播 / 组播 的概念 (1) 单播 (2) 广播 (3) 多播&#xff08;组播&#xff09; 2、广播 socket编程&#xff08;只能是UDP通信&#xff09; 3、多播 socket编程&#xff08;只能是UDP通信&#xff09; 1、单播 / 广播 / 组播 的概念 (1) 单播 之前在进行…