美文网首页issueServer
七牛上传图片实践

七牛上传图片实践

作者: 陌上疏影凉 | 来源:发表于2017-05-18 21:55 被阅读4280次

    最近用到七牛上传视频和图片的功能,于是去七牛官网看了文档,写了一个上传文件到七牛的demo,顺便将写的过程中踩的一些坑记录下来。

    注册七牛开发者账号

    这个就不说了,非常简单,注册完之后,在左边对象存储项下新建一个存储空间,填写存储空间的名字,其他选项直接默认就好了。

    然后在左侧个人中心-密钥管理下查看自己的AccessKey和SecretKey,应该很长一串字母和数字组合,说到这里不得不提一下我的坑。

    我一开始用的qq浏览器打开的这个网页,可能是浏览器记住了我登录的账号密码,然后不知道怎么回事将这里的两个密钥替换成了我的账号密码,我以为密钥就是这个,结果导致后面的token怎么都不对,调试了半天总是bad token的错误,整个人都不好了。所以能用谷歌浏览器还是用谷歌吧。

    七牛云的任务就结束了,还是很简单的,只要创建一个存储空间就好了。

    获得token

    这一步是非常重要的,先不说这个token是怎么得到的,因为算法有点复杂。我们直接用官网提供的工具先生成token供后面测试。最后会给出生成token的代码。

    token在线生成工具

    点击左上角的运行


    会在右下角看到这个


    上一步我们已经看到了两个密钥,将其分别输入到AccessKey和SecretKey中,bucketname就是之前新建的那个存储空间的名字,deadline为这个token的失效时间,其他可以不填,再点击生成uptoken,就会根据这些内容生成一个能供你测试的token。

    这里注意一下你设置的token的有效时间,这里踩的一个坑是测试的时间太长,1个小时后token就失效了,所以自然上传也会出错,所以当你碰到error:null body这个错误时可以试试查看是不是token失效了

    安卓端代码

    七牛安卓sdk文档

    添加依赖

    这里直接添加比较新的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是怎么生成的,可以看看官方文档:

    管理凭证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,所以在写完了之后立马记了下来以备以后不时之需。

    上传demo github地址

    相关文章

      网友评论

      • JefferyzZ:链式上传感觉影响性能,可以采用异步多线程上传
      • 伯纳乌大王:这个递归写的很有灵性,我之前还用的handler:joy:
      • 0a105caa3c54:你好,有分片上传的demo吗?
        陌上疏影凉: @差不多先生ly 压缩上传指的是上传前压缩么?
        0a105caa3c54:@陌上疏影凉 我现在普通文件上传是没问题了。就是我们老大现在叫我做成批量文件分片压缩上传。现在批量文件上传有时候上传不成功,分片的话按照网上的做的感觉好像也不太对。如果可以把批量分片上传也弄一下哈,能压缩是最好了。我弄了几天感觉不太对。:smile:
        陌上疏影凉: @差不多先生ly 这个我那时候做项目没碰到,我先记录下来,等忙完这段时间我去了解一下,重新开始更新博客😂

      本文标题:七牛上传图片实践

      本文链接:https://www.haomeiwen.com/subject/fwrexxtx.html