本文主要讲述在 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 实体类,用于上传 OBS 后的返回结果,代码如下:
class OBSResponse{
String? objectName;
String? fileName;
String? url;
int? size;
String? ext;
String? md5;
}
具体字段说明在上面使用介绍里已经说明了,这里就不过多介绍了。
核心代码都在 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 的路径不支持 / 开始,所以这里做了处理,如果是 / 开始则移除 /。
根据访问域名 domain和 objectName 组装成 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 进行返回。
这样通用的对象上传方法就完成了,接下看看 putObject 和 putFile 的实现:
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
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta