Flutter實現檔案上傳華為物件儲存(OBS)

語言: CN / TW / HK

theme: cyanosis

本文主要講述在 Flutter 專案中如何實現將檔案上傳到華為 OBS(物件儲存)中,並封裝為三方庫方便靈活使用。

背景介紹

在大多專案中都會存在檔案上傳的需求,之前的實現都是呼叫後臺的檔案上傳介面將檔案上傳到伺服器上,但是這樣會存在一個問題,因為檔案上傳會佔用頻寬導致在檔案上傳中呼叫其他介面的時候就會存在訪問慢的情況,解決方案當然是升級頻寬或者單獨使用一臺伺服器作為檔案服務,而且要頻寬足夠大不然上傳下載的時候會很慢,但是這樣兩種方案成本都比較高。隨著雲端計算的到來,各大雲服務商都提供了物件儲存的服務,費用便宜、頻寬高、不影響業務系統而且提供了很多附加功能,比如圖片處理、圖片鑑黃等功能。

因目前在做的專案甲方爸爸明確要求雲服務要使用華為雲,所以物件儲存服務也必須使用華為雲的 OBS 服務,而為了節約人力成本移動端使用的是 Flutter 跨平臺開發,所以就有了本篇文章標題的需求,需要在 Flutter 中實現將檔案上傳到華為雲 OBS 中,而華為雲 OBS 並沒有提供 Flutter SDK,所以就需要自己實現,首先看一下實現以後的程式碼使用效果。

使用

目前只封裝了兩個簡單的功能:上傳物件、上傳檔案。

首先在專案的 pubspec.yaml 裡新增依賴,如下:

flutter_hw_obs:    git:      url: http://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

\