本篇博客谈的是前段时间接触的腾讯云对象存储的集成和具体使用。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);
这一步很重要,也是拦住了很多人的第一个坎,因为腾讯官方用的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 childFile = new ArrayList();
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);
}
PS补充:官方的github链接https://github.com/tencentyun/cos_android_sdk/blob/master/README.md ,官方提供了两套操作,一组同步的一组异步的。