本篇博客談的是前段時間接觸的騰訊雲對象存儲的集成和具體使用。sdk並不複雜,主要是騰訊雲的文檔沒怎麼更新,很多地方講解的也不清晰,我已多次入坑所以想要寫一篇相關的博客,好了進入正題。
基礎版Demo:http://download.csdn.net/detail/g_ying_jie/9909448
功能演進版Demo:http://download.csdn.net/detail/g_ying_jie/9924321
仿微雲終極Demo:http://download.csdn.net/download/g_ying_jie/9959175
第一步、註冊騰訊雲:https://www.qcloud.com/register
第二步、登陸控制檯創建存儲桶:https://console.qcloud.com/cos4/bucket
第三步、下載Android SDK並集成so庫和jar包,配置相關環境和權限(以前的騰訊雲直播~上談過很多了這裏就不再提了),相關官網鏈接
第四步、初始化COSClient對象,對應demo的BizService類有詳細用法
String appid = "騰訊雲註冊的appid";
Context context = getApplicationContext();
String peristenceId = "持久化Id";
//創建COSClientConfig對象,根據需要修改默認的配置參數
COSClientConfig config = new COSClientConfig();
/**
* 設置園區;根據創建的cos空間時選擇的園區
* 華南園區:gz 或 COSEndPoint.COS_GZ(已上線)
* 華北園區:tj 或 COSEndPoint.COS_TJ(已上線)
* 華東園區:sh 或 COSEndPoint.COS_SH
*/
config.setEndPoint(COSEndPoint.COS_GZ);
//創建COSlient對象,實現對象存儲的操作
COSClient cos = new COSClient(context,appid,config,peristenceId);
第五步、生成簽名,詳細用法可以下載demo的BizService類
這一步很重要,也是攔住了很多人的第一個坎,因爲騰訊官方用的PHP作爲示例,更令人無語的是官方提供的Android SDK的demo沒有提到拼接簽名,demo直接請求騰訊的服務器獲取的單次有效和多次有效簽名。廢話不多說,下面介紹Android端如何獲取單次有效和多次有效簽名
①生成單次有效簽名
public String getSignOnce(String cosPath) {
try {
String Original = getSignOriginalOnce(cosPath);
byte[] HmacSHA1 = HmacSHA1Encrypt(SecretKey, Original);
byte[] all = new byte[HmacSHA1.length + Original.getBytes(ENCODING).length];
System.arraycopy(HmacSHA1, 0, all, 0, HmacSHA1.length);
System.arraycopy(Original.getBytes(ENCODING), 0, all, HmacSHA1.length, Original.getBytes(ENCODING).length);
String SignData = Base64Util.encode(all);
return SignData;
} catch (Exception e) {
e.printStackTrace();
}
return "get sign failed";
}
/**
* @param SecretKey 密鑰
* @param EncryptText 簽名串
*/
private byte[] HmacSHA1Encrypt(String SecretKey, String EncryptText) throws Exception {
byte[] data = SecretKey.getBytes(ENCODING);
javax.crypto.SecretKey secretKey = new SecretKeySpec(data, MAC_NAME);
Mac mac = Mac.getInstance(MAC_NAME);
mac.init(secretKey);
byte[] text = EncryptText.getBytes(ENCODING);
return mac.doFinal(text);
}
/**
*getLinuxDateSimple()獲取時間戳,單位秒
* getRandomTenStr()獲取隨機數
*/
private String getSignOriginalOnce(String cosPath) {
return String.format(
"a=%s&b=%s&k=%s&e=%s&t=%s&r=%s&f=%s",
appid,
bucket,
SecretId,
"0",
String.valueOf(getLinuxDateSimple()),
getRandomTenStr(),
"/" + appid + "/" + bucket + "/" + cosPath);
}
②生成多次有效簽名
public String getSign() {
try {
String Original = getSignOriginal();
byte[] HmacSHA1 = HmacSHA1Encrypt(SecretKey, Original);
byte[] all = new byte[HmacSHA1.length + Original.getBytes(ENCODING).length];
System.arraycopy(HmacSHA1, 0, all, 0, HmacSHA1.length);
System.arraycopy(Original.getBytes(ENCODING), 0, all, HmacSHA1.length, Original.getBytes(ENCODING).length);
String SignData = Base64Util.encode(all);
return SignData;
} catch (Exception e) {
e.printStackTrace();
}
return "get sign failed";
}
//e=%s表示簽名到期時間戳
private String getSignOriginal() {
return String.format(
"a=%s&b=%s&k=%s&e=%s&t=%s&r=%s&f=",
appid,
bucket,
SecretId,
String.valueOf(getLinuxDateSimple() + 60),
String.valueOf(getLinuxDateSimple()),
getRandomTenStr());
}
第六步,目錄操作
①創建目錄
此處注意:createDirRequest.setBiz_attr(biz_attr);這個請求參數請不要添加,會報-5999參數出錯的錯誤碼。具體原因不明但肯定不是參數錯誤的原因,因爲其他的目錄包括文件操作都是正常的只有目錄創建會失敗,我用官方的demo同樣存在這個問題,已經反饋給騰訊客服。
/**
* 創建多層目錄 dirName = gu/test; dirName.length()<=20
*/
public void createDir() {
final String dirName = "gu/test";
new Thread(new Runnable() {
@Override
public void run() {
CreateDir.crateDir(bizService, dirName);
}
}).start();
}
public static void crateDir(BizService bizService, String cosPath) {
/** CreateDirRequest 請求對象 */
CreateDirRequest createDirRequest = new CreateDirRequest();
/** 設置Bucket */
createDirRequest.setBucket(bizService.bucket);
/** 設置cosPath :遠程路徑*/
createDirRequest.setCosPath(cosPath);
/** 設置sign: 簽名,此處使用多次簽名 */
createDirRequest.setSign(bizService.getSign());
/** 設置listener: 結果回調 */
createDirRequest.setListener(new ICmdTaskListener() {
@Override
public void onSuccess(COSRequest cosRequest, COSResult cosResult) {
CreateDirResult createDirResult = (CreateDirResult) cosResult;
String result = "目錄創建: ret=" + createDirResult.code + "; msg=" + createDirResult.msg
+ "ctime = " + createDirResult.ctime;
Log.w("XIAO", result);
}
@Override
public void onFailed(COSRequest cosRequest, COSResult cosResult) {
String result = "目錄創建失敗:ret=" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO", result);
}
});
/** 發送請求:執行 */
bizService.cosClient.createDir(createDirRequest);
}
②目錄列表查詢
/**
* 1)bucket根目錄自動展示 cosPath = "/"
* 2)指定目錄下的目錄列表展示 cosPath = "test/"
* 3)目錄前綴查詢,存在的關鍵字 cosPath = "test/" ; listDirRequest.setPrefix("t");
* 4)設置顯示數值,1, 10, 100 最大1000
*/
public void listDir() {
final String dirName = "/";
//前綴查詢的字符串,爲空表示不進行精確查詢
final String prefix = "";
new Thread(new Runnable() {
@Override
public void run() {
ListDir.listDir(bizService, dirName, prefix);
}
}).start();
}
public static void listDir(BizService bizService, String cosPath, String prefix) {
/** ListDirRequest 請求對象 */
ListDirRequest listDirRequest = new ListDirRequest();
/** 設置Bucket */
listDirRequest.setBucket(bizService.bucket);
/** 設置cosPath :遠程路徑*/
listDirRequest.setCosPath(cosPath);
/** 設置num :預查詢的目錄數*/
listDirRequest.setNum(100);
/** 設置content: 透傳字段,首次拉取必須清空。拉取下一頁,需要將前一頁返回值中的context透傳到參數中*/
listDirRequest.setContent("");
/** 設置sign: 簽名,此處使用多次簽名 */
listDirRequest.setSign(bizService.getSign());
/** 設置listener: 結果回調 */
listDirRequest.setListener(new ICmdTaskListener() {
@Override
public void onSuccess(COSRequest cosRequest, COSResult cosResult) {
ListDirResult listObjectResult = (ListDirResult) cosResult;
//文件夾 =》不含有 filesize 或 sha 或 access_url 或 souce_url
StringBuilder stringBuilder = new StringBuilder("目錄列表查詢結果如下:");
stringBuilder.append("code =" + listObjectResult.code + "; msg =" + listObjectResult.msg + "\n");
stringBuilder.append("list是否結束:").append(listObjectResult.listover).append("\n");
stringBuilder.append("content = " + listObjectResult.context + "\n");
if (listObjectResult.infos != null && listObjectResult.infos.size() > 0) {
int length = listObjectResult.infos.size();
String str;
JSONObject jsonObject;
StringBuilder fileStringBuilder = new StringBuilder();
StringBuilder dirStringBuilder = new StringBuilder();
for (int i = 0; i < length; i++) {
str = listObjectResult.infos.get(i);
try {
jsonObject = new JSONObject(str);
if (jsonObject.has("sha")) {
//是文件
fileStringBuilder.append("文件:" + jsonObject.optString("name")).append("\n");
} else {
//是文件夾
dirStringBuilder.append("文件夾: " + jsonObject.optString("name")).append("\n");
}
} catch (JSONException e) {
e.printStackTrace();
}
}
stringBuilder.append(fileStringBuilder);
stringBuilder.append(dirStringBuilder);
} else {
stringBuilder.append("該目錄下無內容");
}
String result = stringBuilder.toString();
Log.w("XIAO", result);
}
@Override
public void onFailed(COSRequest cosRequest, COSResult cosResult) {
String result = "目錄查詢失敗:ret=" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO", result);
}
});
/** 設置 prefix: 前綴查詢的字符串,開啓前綴查詢 */
if (!TextUtils.isEmpty(prefix) && prefix != null) {
listDirRequest.setPrefix(prefix);
}
/** 發送請求:執行 */
bizService.cosClient.listDir(listDirRequest);
}
③目錄刪除
public void deleteDir() {
final String dirName = "empty/";
new Thread(new Runnable() {
@Override
public void run() {
RemoveEmptyDir.removeEmptyDir(bizService, dirName);
}
}).start();
}
public static void removeEmptyDir(BizService bizService, String cosPath) {
/** RemoveEmptyDirRequest 請求對象,只能刪除空文件夾,其他無效 */
RemoveEmptyDirRequest removeEmptyDirRequest = new RemoveEmptyDirRequest();
/** 設置Bucket */
removeEmptyDirRequest.setBucket(bizService.bucket);
/** 設置cosPath :遠程路徑*/
removeEmptyDirRequest.setCosPath(cosPath);
/** 設置sign: 簽名,此處使用單次簽名 */
removeEmptyDirRequest.setSign(bizService.getSignOnce(cosPath));
/** 設置listener: 結果回調 */
removeEmptyDirRequest.setListener(new ICmdTaskListener() {
@Override
public void onSuccess(COSRequest cosRequest, COSResult cosResult) {
String result = "code =" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO", result);
}
@Override
public void onFailed(COSRequest cosRequest, COSResult cosResult) {
String result = "code =" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO", result);
}
});
/** 發送請求:執行 */
bizService.cosClient.removeEmptyDir(removeEmptyDirRequest);
}
深入探究:COS並沒有提供非空文件夾的刪除接口,那麼怎麼實現該功能呢
/**
* 遞歸刪除文件和文件夾
*/
private List<FileItem> childFile = new ArrayList<FileItem>();
private void RecursionDeleteFile(final FileItem item) {
if (item.getType() == 0) {
onDelete(item.getCosPath(), 0);
return;
}
if (item.getType() == 1) {
//前綴查詢的字符串,爲空表示不進行精確查詢
final String prefix = "";
ListDirRequest dirRequest = DirUtil.getListDirRequest(bizService, item.getCosPath(), prefix);
dirRequest.setListener(new ICmdTaskListener() {
@Override
public void onSuccess(COSRequest cosRequest, COSResult cosResult) {
if (!childFile.isEmpty())
childFile.clear();
DirUtil.getData(cosResult, childFile, cosRequest.getCosPath());
if (childFile == null || childFile.size() == 0) {
onDelete(item.getCosPath(), 1);
return;
}
for (FileItem f : childFile) {
RecursionDeleteFile(f);
}
onDelete(item.getCosPath(), 1);
}
@Override
public void onFailed(COSRequest cosRequest, COSResult cosResult) {
Log.e("DELETE", cosResult.code + "==" + cosResult.msg);
}
});
bizService.cosClient.listDirAsyn(dirRequest);
}
}
private void onDelete(String path, int type) {
if (type == 0) {
DeleteObjectRequest request = ObjectUtil.getDeleteObjRequest(bizService, path);
request.setListener(this);
bizService.cosClient.deleteObjectAsyn(request);
}
if (type == 1) {
if (path.equals("/doc/") || path.equals("/music/") || path.equals("/picture/") || path.equals("/video/"))
return;
RemoveEmptyDirRequest request = DirUtil.getRemoveDirRequest(bizService, path);
request.setListener(this);
bizService.cosClient.removeEmptyDirAsyn(request);
}
}
其中可以用Javabean類的String屬性存儲列表查詢所得的cosPath以及標記類型(是文件夾或者文件),然後遞歸刪除,具體刪除某項的時候通過類型調用對應的刪除方法①文件上傳,這裏是第二個坑,騰訊的demo只能夠選取文件不能選取音樂,圖片等資源
首先跳轉自帶的文件管理器,選定要上傳的內容
protected void onAdd() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
//intent.setType(“image/*”);
//intent.setType(“audio/*”); //選擇音頻
//intent.setType(“video/*”); //選擇視頻 (mp4 3gp 是android支持的視頻格式)
//intent.setType(“video/*;image/*”);//同時選擇視頻和圖片
intent.setType("*/*");//無類型限制
intent.addCategory(Intent.CATEGORY_OPENABLE);
try {
startActivityForResult(intent, OPENFILE_CODE);
} catch (android.content.ActivityNotFoundException ex) {
Toast.makeText(this, "親,木有文件管理器啊-_-!!", Toast.LENGTH_SHORT).show();
}
}
然後在onActivityResult獲取返回的URI並解析成文件的絕對路徑
注意:Android 4.4之後獲取的URI與之前的不一樣,單純的uri.getPath()並不能正確獲取選中文件的絕對路徑,需要按照如下方法處理。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK || data == null) {
return;
}
switch (requestCode) {
case OPENFILE_CODE:
Uri uri = data.getData();
currentPath = getPath(this, uri);
localText.setText(currentPath);
break;
default:
break;
}
}
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {column};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
最後上傳文件,騰訊雲對象存儲支持斷點續傳,默認的使用大文件分片上傳的方式會開啓斷點續傳。另小文件簡單上傳可以下載demo,對應的PutObject類有介紹
public void upload2() {
if (TextUtils.isEmpty(currentPath)) {
Toast.makeText(FileUploadActivity.this, "請選擇文件", Toast.LENGTH_SHORT).show();
return;
}
new Thread(new Runnable() {
@Override
public void run() {
String filename = FileUtils.getFileName(currentPath);
String cosPath = "/" + filename; //cos 上的路徑
PutObject.putObjectForLargeFile(bizService, cosPath, currentPath);
}
}).start();
}
/**
* 大文件分片上傳 : >=20M的文件,需要使用分片上傳,否則會出錯
*/
public static void putObjectForLargeFile(BizService bizService, String cosPath, String localPath) {
/** PutObjectRequest 請求對象 */
PutObjectRequest putObjectRequest = new PutObjectRequest();
/** 設置Bucket */
putObjectRequest.setBucket(bizService.bucket);
/** 設置cosPath :遠程路徑*/
putObjectRequest.setCosPath(cosPath);
/** 設置srcPath: 本地文件的路徑 */
putObjectRequest.setSrcPath(localPath);
/** 設置 insertOnly: 是否上傳覆蓋同名文件*/
putObjectRequest.setInsertOnly("1");
/** 設置sign: 簽名,此處使用多次簽名 */
putObjectRequest.setSign(bizService.getSign());
/** 設置sliceFlag: 是否開啓分片上傳 */
putObjectRequest.setSliceFlag(true);
/** 設置slice_size: 若使用分片上傳,設置分片的大小 */
putObjectRequest.setSlice_size(1024 * 1024);
/** 設置sha: 是否上傳文件時帶上sha,一般帶上sha*/
putObjectRequest.setSha(SHA1Utils.getFileSha1(localPath));
/** 設置listener: 結果回調 */
putObjectRequest.setListener(new IUploadTaskListener() {
@Override
public void onProgress(COSRequest cosRequest, long currentSize, long totalSize) {
long progress = ((long) ((100.00 * currentSize) / totalSize));
Log.w("XIAO", "progress =" + progress + "%");
}
@Override
public void onCancel(COSRequest cosRequest, COSResult cosResult) {
String result = "上傳出錯: ret =" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO", result);
}
@Override
public void onSuccess(COSRequest cosRequest, COSResult cosResult) {
PutObjectResult putObjectResult = (PutObjectResult) cosResult;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(" 上傳結果: ret=" + putObjectResult.code + "; msg =" + putObjectResult.msg + "\n");
stringBuilder.append(" access_url= ");
stringBuilder.append(putObjectResult.access_url == null ? "null" : putObjectResult.access_url + "\n");
stringBuilder.append(" resource_path= ");
stringBuilder.append(putObjectResult.resource_path == null ? "null" : putObjectResult.resource_path + "\n");
stringBuilder.append(" url= ");
stringBuilder.append(putObjectResult.url == null ? "null" : putObjectResult.url);
String result = stringBuilder.toString();
Log.w("XIAO", result);
}
@Override
public void onFailed(COSRequest cosRequest, COSResult cosResult) {
String result = "上傳出錯: ret =" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO", result);
}
});
/** 發送請求:執行 */
bizService.cosClient.putObject(putObjectRequest);
}
②文件下載
public void onDownload() {
final String savePath = Environment.getExternalStorageDirectory().getAbsolutePath() +
File.separator + "test_download";
final String downloadUrl = "http://demo-1253703400.cosgz.myqcloud.com/bd_etts_text.dat";
resultText.setText("download_url :" + downloadUrl + "\n" + "savePath :" + savePath);
new Thread(new Runnable() {
@Override
public void run() {
GetObject.getObject(bizService, downloadUrl, savePath);
}
}).start();
}
public static void getObject(BizService bizService, String url, String savePath){
/** GetObjectRequest 請求對象 */
GetObjectRequest getObjectRequest = new GetObjectRequest(url,savePath);
//若是設置了防盜鏈則需要簽名;否則,不需要
/** 設置listener: 結果回調 */
getObjectRequest.setListener(new IDownloadTaskListener() {
@Override
public void onProgress(COSRequest cosRequest, long currentSize, long totalSize) {
long progress = (long) ((100.00 * currentSize) / totalSize);
Log.w("XIAO","progress =" + progress + "%");
}
@Override
public void onCancel(COSRequest cosRequest, COSResult cosResult) {
String result = "cancel =" + cosResult.msg;
Log.w("XIAO",result);
}
@Override
public void onSuccess(COSRequest cosRequest, COSResult cosResult) {
String result = "code =" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO",result);
}
@Override
public void onFailed(COSRequest cosRequest, COSResult cosResult) {
String result ="code =" + cosResult.code + "; msg =" + cosResult.msg;
Log.w("XIAO",result);
}
});
bizService.cosClient.getObject(getObjectRequest);
}
注意:我們上傳的如果是中文名文件,服務器生成下載鏈接時會對中文進行轉碼,形成如%E9%之類的一串字符
如果用瀏覽器下載此鏈接地址,當然不會有問題,瀏覽器會自動轉碼
但是問題來了,如果調用騰訊雲API的cosClient.getObject(getObjectRequest)或者cosClient.getObjectAsyn(getObjectRequest)
騰訊這裏會調用OKhttp去執行下載任務,並不會幫我們實現轉碼,所以我們下載成功的文件是這樣的
解決方案就是:在下載成功的onSuccess回調裏將文件名轉碼回中文,方法如下
private static void renameFile(String url, String savePath) {
String str = url.substring(url.lastIndexOf("/") + 1);
StringBuilder localUrl = new StringBuilder(savePath);
StringBuilder destUrl = new StringBuilder(savePath);
String fileName = null;
try {
fileName = URLDecoder.decode(str, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
localUrl.append(File.separator).append(str);
destUrl.append(File.separator).append(fileName);
File file = new File(localUrl.toString());
File destFile = new File(destUrl.toString());
file.renameTo(destFile);
}
這估計是我目前最長的一篇博客了,博主比較懶沒啥耐心喜歡直接貼代碼,不怎麼喜歡細緻講解,所以還有一些功能就不一一羅列了,比如目錄查詢、文件查詢、文件更新、文件刪除、文件複製、文件移動,這些在我上傳的demo都有示例用法。有什麼錯誤歡迎留言指正,有問題也可以留言相互討論交流。
PS補充:官方的github鏈接https://github.com/tencentyun/cos_android_sdk/blob/master/README.md ,官方提供了兩套操作,一組同步的一組異步的。