最近用到七牛上传视频和图片的功能,于是去七牛官网看了文档,写了一个上传文件到七牛的demo,顺便将写的过程中踩的一些坑记录下来。
注册七牛开发者账号
这个就不说了,非常简单,注册完之后,在左边对象存储项下新建一个存储空间,填写存储空间的名字,其他选项直接默认就好了。
然后在左侧个人中心-密钥管理下查看自己的AccessKey和SecretKey,应该很长一串字母和数字组合,说到这里不得不提一下我的坑。
我一开始用的qq浏览器打开的这个网页,可能是浏览器记住了我登录的账号密码,然后不知道怎么回事将这里的两个密钥替换成了我的账号密码,我以为密钥就是这个,结果导致后面的token怎么都不对,调试了半天总是bad token的错误,整个人都不好了。所以能用谷歌浏览器还是用谷歌吧。
七牛云的任务就结束了,还是很简单的,只要创建一个存储空间就好了。
获得token
这一步是非常重要的,先不说这个token是怎么得到的,因为算法有点复杂。我们直接用官网提供的工具先生成token供后面测试。最后会给出生成token的代码。
点击左上角的运行
会在右下角看到这个
上一步我们已经看到了两个密钥,将其分别输入到AccessKey和SecretKey中,bucketname就是之前新建的那个存储空间的名字,deadline为这个token的失效时间,其他可以不填,再点击生成uptoken,就会根据这些内容生成一个能供你测试的token。
这里注意一下你设置的token的有效时间,这里踩的一个坑是测试的时间太长,1个小时后token就失效了,所以自然上传也会出错,所以当你碰到error:null body
这个错误时可以试试查看是不是token失效了
安卓端代码
添加依赖
这里直接添加比较新的7.3.x,后面会说为什么
compile 'com.qiniu:qiniu-android-sdk:7.3.2'
发送请求
首先看看文档是怎么写的
要准备3个内容,data、key、token
- token ,这个是最简单的,上一步生成的token直接可以拿过来用
- key , 这个是指定你的图片或其他文件上传到七牛存储空间后叫什么名字
- data , 这个是要上传的目标,可以是File类型的文件,可以是String类型的文件所在目录,也可是是byte[]数组,上传图片的话肯定是用前两种比较方便
回调函数的参数文档中也有
在调试的时候可以将info打印到日志中,这样如果没有上传成功也可以看到是什么原因。如果不主动打印日志,那么上传失败,AS是不会打印任何错误信息的。
这里以上传手机中的一张图片为例,图片位置在手机sd卡目录下test.jpg。可仿照文档写出以下代码:
new Thread(new Runnable() {
@Override
public void run() {
UploadManager uploadManager = new UploadManager();
String path = Environment.getExternalStorageDirectory() + "/test.jpg";
File file = new File(path);
uploadManager.put(file, null, upToken,
new UpCompletionHandler() {
@Override
public void complete(String key, ResponseInfo respInfo,
JSONObject jsonData) {
if (respInfo.isOK()) {
Toast.makeText(MainActivity.this, "上传成功!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "上传失败!", Toast.LENGTH_SHORT).show();
Log.d(TAG, "error: " + respInfo.toString());
}
}
}, null);
}
}).start();
如果你这就急急忙忙准备运行,而又不打印任何日志,你会发现总是上传失败,并且还找不到原因。一开始我也是一脸茫然,后来把info的信息打印出来之后看到access denied,立马就知道了忘记设置权限了。
这里一定不要忘记给app加上读取sd卡和联网的权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
加了权限这下该没问题了吧,说不准,之前我们说最好是用7.3.x版本,因为我在用7.0.x版本的时候遇到一个错误
error:incorrect region, please use up-z2.qiniu.com
这个错误跟地区有关,在华南地区需要用up-z2.qiniu.com
这个域名去访问。在文档中之前被我忽略的一部分派上用场了
可以在创建UploadManager
时给它传入一些设置。
Configuration config = new Configuration.Builder()
.zone(Zone.zone2) // 设置区域,指定不同区域的上传域名、备用域名、备用IP。
.build();
UploadManager uploadManager = new UploadManager(config);
Configuration config = new Configuration.Builder()
.zone(Zone.httpAutoZone) // 自动识别
.build();
UploadManager uploadManager = new UploadManager(config);
点开Zone我们就知道为什么设置这个能解决问题了,出现的问题就是让我们使用up-z2.qiniu.com
,而这个域名就在Zone.zone2里。
public abstract class Zone {
/**
* 华东机房, http
*/
public static final Zone zone0 =
createZone("upload.qiniu.com", "up.qiniu.com", "183.136.139.10", "115.231.182.136");
/**
* 华北机房, http
*/
public static final Zone zone1 =
createZone("upload-z1.qiniu.com", "up-z1.qiniu.com", "106.38.227.27", "106.38.227.28");
/**
* 华南机房, http
*/
public static final Zone zone2 =
createZone("upload-z2.qiniu.com", "up-z2.qiniu.com", "183.60.214.197", "14.152.37.7");
/**
* 北美机房, http
*/
public static final Zone zoneNa0 =
createZone("upload-na0.qiniu.com", "up-na0.qiniu.com", "23.236.102.3", "23.236.102.2");
/**
* 自动判断机房, http
*/
public static final AutoZone httpAutoZone = new AutoZone(false, null);
/**
* 自动判断机房, https
*/
public static final AutoZone httpsAutoZone = new AutoZone(true, null);
...
}
结果
上传成功后,在test-demo存储空间中就会增加一张新的图片了
获取图片的外链地址
打开七牛云端,test-demo内容管理可以查看已上传的文件,点击可以查看外链地址:
这个外链地址可以通过domain+key的组合来得到,domain也能在内容管理查看到:
key就是上传后在七牛存储上的文件名。
所以组合后的地址就是http://oq543v9g0.bkt.clouddn.com/lt0EG7Sm0nVKu3YaZAhE9XRoKgBr
token的加密算法
关于token是怎么生成的,可以看看官方文档:
这里只提供加密的代码,可以对照着官方文档看看是如何加密的:
//七牛后台的key
private static String AccessKey = "AccessKey";
//七牛后台的secret
private static String SecretKey = "SecretKey";
private static final String MAC_NAME = "HmacSHA1";
private static final String ENCODING = "UTF-8";
public String getToken(){
JSONObject json = new JSONObject();
long deadline = System.currentTimeMillis() / 1000 + 3600;
try {
json.put("deadline", deadline);// 有效时间为一个小时
json.put("scope", bucketName);//存储空间的名字
} catch (JSONException e) {
e.printStackTrace();
}
String encodedPutPolicy = UrlSafeBase64.encodeToString(json
.toString().getBytes());
byte[] sign = new byte[0];
try {
sign = HmacSHA1Encrypt(encodedPutPolicy, SecretKey);
} catch (Exception e) {
e.printStackTrace();
}
String encodedSign = UrlSafeBase64.encodeToString(sign);
String uploadToken = AccessKey + ':' + encodedSign + ':'
+ encodedPutPolicy;
return uploadToken;
}
public static byte[] HmacSHA1Encrypt(String encryptText, String encryptKey)
throws Exception {
byte[] data = encryptKey.getBytes(ENCODING);
// 根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称
SecretKeySpec secretKey = new SecretKeySpec(data, MAC_NAME);
// 生成一个指定 Mac 算法 的 Mac 对象
Mac mac = Mac.getInstance(MAC_NAME);
// 用给定密钥初始化 Mac 对象
mac.init(secretKey);
byte[] text = encryptText.getBytes(ENCODING);
// 完成 Mac 操作
return mac.doFinal(text);
}
进阶用法:多图上传
上述代码只能完成一张图片的上传,而做项目的时候经常需要一次性上传多张图片,这里提供两种实现方法。
定义的全局变量:
private String upToken = "你的token";
private Configuration config = new Configuration.Builder()
.zone(Zone.zone2)
.build();
private UploadManager uploadManager = new UploadManager(config);
private int[] i = {0};//循环变量,表示现在正在上传第几张图片
循环实现
优点:实现简单
缺点:顺序不好控制
new Thread(new Runnable() {
@Override
public void run() {
//两张图片路径
String path1 = Environment.getExternalStorageDirectory() + "/test.jpg";
String path2 = Environment.getExternalStorageDirectory() + "/test-1.jpg";
final List<String> list = new ArrayList<>();
list.add(path1);
list.add(path2);
for(i[0]=0;i[0]<list.size();i[0]++) {
String file = list.get(i[0]);
uploadManager.put(file, null, upToken,
new UpCompletionHandler() {
@Override
public void complete(String key, ResponseInfo respInfo,
JSONObject jsonData) {
if (respInfo.isOK()) {
print("第" + i[0] +"张上传成功!");
} else {
print("第" + i[0] +"张上传失败!");
Log.d(TAG, "error: " + respInfo.error);
}
}
}, null);
}
}
}).start();
运行结果:
说明:
问题出来了,为什么打印了两个“第2张上传成功”?原因是因为是循环进行网络请求,效果如下图:
网络请求是需要消耗时间的,而循环在开启一个网络请求upload1后就立马进入下一个循环了(而不会等待网络请求返回结果)。这时,循环变量已经由0变为1,但upload1可能还没有返回结果,这时开启第二个网络请求upload2,所以等两个请求都完成时,循环变量已经变为1,因而两个请求返回结果时都会打印“第2张上传成功”。
递归实现
优点:顺序清晰
缺点:代码多,复杂
private void click() {
//两张图片路径
String path1 = Environment.getExternalStorageDirectory() + "/test.jpg";
String path2 = Environment.getExternalStorageDirectory() + "/test-1.jpg";
final List<String> list = new ArrayList<>();
list.add(path1);
list.add(path2);
//递归上传两张图片
uploadMutliFiles(list, new UploadMutliListener() {
@Override
public void onUploadMutliSuccess() {
print(list.size() + "张图片上传成功!");
}
@Override
public void onUploadMutliFail(Error error) {
print("上传失败!");
}
});
}
//上传多张图片
public void uploadMutliFiles(final List<String> filesUrls, final UploadMutliListener uploadMutliListener) {
if (filesUrls != null && filesUrls.size() > 0) {
final String url = filesUrls.get(i[0]);
uploadFile(url, new UploadListener() {
@Override
public void onUploadSuccess() {
final UploadListener uploadListener = this;
Log.d(TAG, "第" + (i[0]+1) + "张:" + url + "\t上传成功!");
i[0]++;
//递归边界条件
if (i[0] < filesUrls.size()) {
//七牛后台对上传的文件名是以时间戳来命名,以秒为单位,如果文件上传过快,两张图片就会重名而上传失败,所以间隔1秒,保证上传成功(具体会不会失败呢?自己试一下看看)
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
uploadFile(filesUrls.get(i[0]), uploadListener);
}
}, 1000);
} else {
uploadMutliListener.onUploadMutliSuccess();
}
}
@Override
public void onUploadFail(Error error) {
print("第" + (i[0]+1) + "张上传失败!" + filesUrls.get(i[0]));
uploadMutliListener.onUploadMutliFail(error);
}
});
}
}
//上传单个文件
public void uploadFile(final String filePath, final UploadListener uploadListener) {
if (filePath == null) return;
new Thread(new Runnable() {
@Override
public void run() {
if (uploadManager == null) {
uploadManager = new UploadManager();
}
uploadManager.put(filePath, null, upToken,
new UpCompletionHandler() {
@Override
public void complete(String key, ResponseInfo respInfo,
JSONObject jsonData) {
if (respInfo.isOK()) {
print(jsonData.toString());
uploadListener.onUploadSuccess();
} else {
uploadListener.onUploadFail(new Error("上传失败" + respInfo.error));
}
}
}, null);
}
}).start();
}
//上传回调
public interface UploadListener {
void onUploadSuccess();
void onUploadFail(Error error);
}
//上传多张文件回调
public interface UploadMutliListener {
void onUploadMutliSuccess();
void onUploadMutliFail(Error error);
}
运行结果:
说明:
看代码规模就能明显感觉两者的不同,递归上传代码量相比循环多了很多,不过它的优点就是它的顺序十分清晰,递归调用的逻辑图可以描绘成以下的效果:
为每个网络请求upload设置一个监听器,只有当upload1的请求成功返回结果,也就是第一张图片上传成功后,才开启第2个上传图片的请求upload2,如果有upload3同样接在upload2的success后,这样形成了链式结构,能确保图片一定是按照顺序上传的。
总结
七牛云不局限于上传图片,其实任何文件都可以,所以如果是上传视频的话,道理都是一样的。
七牛上传文件的过程并不是很难,但是在网上找了一圈没有找到合适的demo,所以在写完了之后立马记了下来以备以后不时之需。