Browse Source

cos换七牛

lizhen_gitee 1 year ago
parent
commit
a9aba42e35

File diff suppressed because it is too large
+ 0 - 0
addons/cos/assets/js/spark.js


+ 1 - 0
addons/qiniu/.addonrc

@@ -0,0 +1 @@
+{"files":[],"license":"regular","licenseto":"19079","licensekey":"7KfOcrz46MPWyZ02 K3KCaHJuupFjf7NkoLf\/FA==","domains":[],"licensecodes":[],"validations":[]}

+ 128 - 0
addons/qiniu/Qiniu.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace addons\qiniu;
+
+use fast\Http;
+use Qiniu\Auth;
+use think\Addons;
+use think\App;
+use think\Loader;
+
+/**
+ * 七牛云储存插件
+ */
+class Qiniu extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+    /**
+     * 判断是否来源于API上传
+     */
+    public function moduleInit($request)
+    {
+        $config = $this->getConfig();
+        $module = strtolower($request->module());
+        if ($module == 'api' && ($config['apiupload'] ?? 0) &&
+            strtolower($request->controller()) == 'common' &&
+            strtolower($request->action()) == 'upload') {
+            request()->param('isApi', true);
+            App::invokeMethod(["\\addons\\qiniu\\controller\\Index", "upload"], ['isApi' => true]);
+        }
+    }
+
+    /**
+     * 上传初始化时
+     */
+    public function uploadConfigInit(&$upload)
+    {
+        $config = $this->getConfig();
+        $module = request()->module();
+        $module = $module ? strtolower($module) : 'index';
+
+        $policy = array(
+            'saveKey' => ltrim($config['savekey'], '/'),
+        );
+
+        $config['savekey'] = str_replace(
+            ['$(year)', '$(mon)', '$(day)', '$(hour)', '$(min)', '$(sec)', '$(etag)', '$(ext)', '$(fname)'],
+            ['{year}', '{mon}', '{day}', '$(hour)', '$(min)', '$(sec)', '{filemd5}', '{.suffix}', '{filename}'],
+            $config['savekey']
+        );
+        $auth = new Auth($config['accessKey'], $config['secretKey']);
+
+        $token = '';
+        if (\addons\qiniu\library\Auth::isModuleAllow()) {
+            $token = $auth->uploadToken($config['bucket'], null, $config['expire'], $policy);
+        }
+        $multipart = [
+            'qiniutoken' => $token
+        ];
+
+        $upload = array_merge($upload, [
+            'cdnurl'     => $config['cdnurl'],
+            'uploadurl'  => $config['uploadmode'] == 'client' ? $config['uploadurl'] : addon_url('qiniu/index/upload', [], false, true),
+            'uploadmode' => $config['uploadmode'],
+            'bucket'     => $config['bucket'],
+            'maxsize'    => $config['maxsize'],
+            'mimetype'   => $config['mimetype'],
+            'savekey'    => $config['savekey'],
+            'chunking'   => (bool)($config['chunking'] ?? $upload['chunking']),
+            'chunksize'  => (int)($config['chunksize'] ?? $upload['chunksize']),
+            'multipart'  => $multipart,
+            'storage'    => $this->getName(),
+            'multiple'   => (bool)$config['multiple'],
+        ]);
+    }
+
+    /**
+     * 附件删除后
+     */
+    public function uploadDelete($attachment)
+    {
+        $config = $this->getConfig();
+        if ($attachment['storage'] == 'qiniu' && isset($config['syncdelete']) && $config['syncdelete']) {
+            $auth = new Auth($config['accessKey'], $config['secretKey']);
+            $entry = $config['bucket'] . ':' . ltrim($attachment->url, '/');
+            $encodedEntryURI = \Qiniu\base64_urlSafeEncode($entry);
+            $url = 'http://rs.qiniu.com/delete/' . $encodedEntryURI;
+            $headers = $auth->authorization($url);
+            //删除云储存文件
+            $ret = Http::sendRequest($url, [], 'POST', [CURLOPT_HTTPHEADER => ['Authorization: ' . $headers['Authorization']]]);
+
+            //如果是服务端中转,还需要删除本地文件
+            //if ($config['uploadmode'] == 'server') {
+            //    $filePath = ROOT_PATH . 'public' . str_replace('/', DS, $attachment->url);
+            //    if ($filePath) {
+            //        @unlink($filePath);
+            //    }
+            //}
+        }
+        return true;
+    }
+
+    public function appInit()
+    {
+        if (!class_exists('\Qiniu\Config')) {
+            Loader::addNamespace('Qiniu', ADDON_PATH . 'qiniu' . DS . 'library' . DS . 'Qiniu' . DS);
+            require_once ADDON_PATH . 'qiniu' . DS . 'library' . DS . 'Qiniu' . DS . 'functions.php';
+        }
+    }
+
+}

+ 187 - 0
addons/qiniu/bootstrap.js

@@ -0,0 +1,187 @@
+//修改上传的接口调用
+require(['upload'], function (Upload) {
+
+    var _onInit = Upload.events.onInit;
+    //初始化中完成判断
+    Upload.events.onInit = function () {
+        _onInit.apply(this, Array.prototype.slice.apply(arguments));
+        //如果上传接口不是七牛云,则不处理
+        if (this.options.url !== Config.upload.uploadurl) {
+            return;
+        }
+        var _success = this.options.success;
+
+        $.extend(this.options, {
+            //关闭自动处理队列功能
+            autoQueue: false,
+            chunkSuccess: function (chunk, file, response) {
+                this.contexts = this.contexts ? this.contexts : [];
+                this.contexts.push(typeof response.ctx !== 'undefined' ? response.ctx : response.data.ctx);
+            },
+            chunksUploaded: function (file, done) {
+                var that = this;
+                var params = $(that.element).data("params") || {};
+                var category = typeof params.category !== 'undefined' ? params.category : ($(that.element).data("category") || '');
+                category = typeof category === 'function' ? category.call(this, file) : category;
+                Fast.api.ajax({
+                    url: "/addons/qiniu/index/upload",
+                    data: {
+                        action: 'merge',
+                        filesize: file.size,
+                        filename: file.name,
+                        chunkid: file.upload.uuid,
+                        chunkcount: file.upload.totalChunkCount,
+                        width: file.width || 0,
+                        height: file.height || 0,
+                        type: file.type,
+                        category: category,
+                        qiniutoken: Config.upload.multipart.qiniutoken,
+                        contexts: this.contexts
+                    },
+                }, function (data, ret) {
+                    done(JSON.stringify(ret));
+                    return false;
+                }, function (data, ret) {
+                    file.accepted = false;
+                    that._errorProcessing([file], ret.msg);
+                    return false;
+                });
+
+            },
+        });
+
+        //先移除已有的事件
+        this.off("success", _success).on("success", function (file, response) {
+            var that = this;
+            var ret = {code: 0, msg: response};
+            try {
+                ret = typeof response === 'string' ? JSON.parse(response) : response;
+                if (file.xhr.status === 200) {
+                    if (Config.upload.uploadmode === 'client') {
+                        if (typeof ret.key !== 'undefined') {
+                            ret = {code: 1, msg: "", data: {url: '/' + ret.key, hash: ret.hash}};
+                        }
+                        var url = ret.data.url || '';
+                        var params = $(that.element).data("params") || {};
+                        var category = typeof params.category !== 'undefined' ? params.category : ($(that.element).data("category") || '');
+                        category = typeof category === 'function' ? category.call(that, file) : category;
+                        Fast.api.ajax({
+                            url: "/addons/qiniu/index/notify",
+                            data: {name: file.name, url: ret.data.url, hash: ret.data.hash, size: file.size, width: file.width || 0, height: file.height || 0, type: file.type, category: category, qiniutoken: Config.upload.multipart.qiniutoken}
+                        }, function () {
+                            return false;
+                        }, function () {
+                            return false;
+                        });
+                    } else {
+                        console.error(ret);
+                    }
+                } else {
+                    console.error(file.xhr);
+                }
+            } catch (e) {
+                console.error(e);
+            }
+            _success.call(this, file, ret);
+        });
+
+        this.on("addedfile", function (file) {
+            var that = this;
+            setTimeout(function () {
+                if (file.status === 'error') {
+                    return;
+                }
+
+                var md5 = ''; //七牛云无需本地获取文件MD5
+                var chunk = that.options.chunking && file.size > that.options.chunkSize ? 1 : 0;
+                var params = $(that.element).data("params") || {};
+                var category = typeof params.category !== 'undefined' ? params.category : ($(that.element).data("category") || '');
+                category = typeof category === 'function' ? category.call(that, file) : category;
+                Fast.api.ajax({
+                    url: "/addons/qiniu/index/params",
+                    data: {method: 'POST', category: category, md5: md5, name: file.name, type: file.type, size: file.size, chunk: chunk, chunksize: that.options.chunkSize, qiniutoken: Config.upload.multipart.qiniutoken},
+                }, function (data) {
+                    file.qiniutoken = data.qiniutoken;
+                    file.params = data;
+                    file.category = category;
+
+                    if (file.status != 'error') {
+                        //开始上传
+                        that.enqueueFile(file);
+                    } else {
+                        that.removeFile(file);
+                    }
+                    return false;
+                }, function () {
+                    that.removeFile(file);
+                });
+            }, 0);
+        });
+
+        //如果是直传模式
+        if (Config.upload.uploadmode === 'client') {
+            var _url = this.options.url;
+
+            //分片上传时URL链接不同
+            this.options.url = function (files) {
+                this.options.headers = {"Authorization": "UpToken " + Config.upload.multipart.qiniutoken};
+                if (files[0].upload.chunked) {
+                    var chunk = null;
+                    files[0].upload.chunks.forEach(function (item) {
+                        if (item.status === 'uploading') {
+                            chunk = item;
+                        }
+                    });
+                    if (!chunk) {
+                        return Config.upload.uploadurl + '/mkfile/' + files[0].size;
+                    } else {
+                        return Config.upload.uploadurl + '/mkblk/' + chunk.dataBlock.data.size;
+                    }
+                }
+                return _url;
+            };
+
+            this.options.params = function (files, xhr, chunk) {
+                var params = Config.upload.multipart;
+                if (chunk) {
+                    return $.extend({}, params, {
+                        filesize: chunk.file.size,
+                        filename: chunk.file.name,
+                        chunkid: chunk.file.upload.uuid,
+                        chunkindex: chunk.index,
+                        chunkcount: chunk.file.upload.totalChunkCount,
+                        chunkfilesize: chunk.dataBlock.data.size,
+                        width: chunk.file.width || 0,
+                        height: chunk.file.height || 0,
+                        type: chunk.file.type,
+                    });
+                } else {
+                    var retParams = $.extend({}, params, files[0].params || {});
+                    //七牛云直传使用的是token参数
+                    retParams.token = retParams.qiniutoken;
+                    delete retParams.qiniutoken;
+                    return retParams;
+                }
+            };
+
+            //分片上传时需要变更提交的内容
+            this.on("sending", function (file, xhr, formData) {
+                if (file.upload.chunked) {
+                    var _send = xhr.send;
+                    xhr.send = function () {
+                        var chunk = null;
+                        file.upload.chunks.forEach(function (item) {
+                            if (item.status == 'uploading') {
+                                chunk = item;
+                            }
+                        });
+                        if (chunk) {
+                            _send.call(xhr, chunk.dataBlock.data);
+                        }
+                    };
+                }
+            });
+        }
+    };
+
+});

+ 243 - 0
addons/qiniu/config.php

@@ -0,0 +1,243 @@
+<?php
+
+return [
+    [
+        'name' => 'accessKey',
+        'title' => 'accessKey',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'xE4ab79m0Wzx3Y54CuMj9BWvNBDzwE2ClWyV7Qe3',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请在个人中心 > 密钥管理中获取 > AK',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'secretKey',
+        'title' => 'secretKey',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'vd2-0wEm7pusAV-SumMO3fu1v8qGfyoiOHvFjcKx',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '请在个人中心 > 密钥管理中获取 > SK',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'bucket',
+        'title' => 'bucket',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'tken',
+        'rule' => 'required;bucket',
+        'msg' => '',
+        'tip' => '存储空间名称',
+        'ok' => '',
+        'extend' => 'data-rule-bucket="[/^[0-9a-z_\\-]{3,63}$/, \'请输入正确的Bucket名称\']"',
+    ],
+    [
+        'name' => 'uploadurl',
+        'title' => '上传接口地址',
+        'type' => 'select',
+        'content' => [
+            'https://upload.qiniup.com' => '华东 https://upload.qiniup.com',
+            'https://upload-z1.qiniup.com' => '华北 https://upload-z1.qiniup.com',
+            'https://upload-z2.qiniup.com' => '华南 https://upload-z2.qiniup.com',
+            'https://upload-na0.qiniup.com' => '北美-洛杉矶 https://upload-na0.qiniup.com',
+            'https://upload-as0.qiniup.com' => '亚太-新加坡 https://upload-as0.qiniup.com',
+            'https://up-cn-east-2.qiniup.com' => '华东 浙江2 https://up-cn-east-2.qiniup.com',
+        ],
+        'value' => 'https://upload.qiniup.com',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '推荐选择最近的地址',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'cdnurl',
+        'title' => 'CDN地址',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'https://oss.tken.vip',
+        'rule' => 'required;cdnurl',
+        'msg' => '',
+        'tip' => '未绑定CDN的话可使用七牛分配的测试域名',
+        'ok' => '',
+        'extend' => 'data-rule-cdnurl="[/^http(s)?:\\/\\/.*$/, \'必需以http(s)://开头\']"',
+    ],
+    [
+        'name' => 'uploadmode',
+        'title' => '上传模式',
+        'type' => 'select',
+        'content' => [
+            'client' => '客户端直传(速度快,无备份)',
+            'server' => '服务器中转(占用服务器带宽,可备份)',
+        ],
+        'value' => 'client',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '启用服务器中转时务必配置操作员和密码',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'serverbackup',
+        'title' => '服务器中转模式备份',
+        'type' => 'radio',
+        'content' => [
+            1 => '备份(附件管理将产生2条记录)',
+            0 => '不备份',
+        ],
+        'value' => '0',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '服务器中转模式下是否备份文件',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'savekey',
+        'title' => '保存文件名',
+        'type' => 'string',
+        'content' => [],
+        'value' => '/uploads/{year}{mon}{day}/{filemd5}{.suffix}',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'expire',
+        'title' => '上传有效时长',
+        'type' => 'string',
+        'content' => [],
+        'value' => '600',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '用户停留页面上传有效时长,单位秒',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'maxsize',
+        'title' => '最大可上传',
+        'type' => 'string',
+        'content' => [],
+        'value' => '200m',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'mimetype',
+        'title' => '可上传后缀格式',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'jpg,png,bmp,jpeg,gif,zip,rar,xls,xlsx,wav,mp4,mp3,pdf,svga',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'multiple',
+        'title' => '多文件上传',
+        'type' => 'radio',
+        'content' => [
+            1 => '开启',
+            0 => '关闭',
+        ],
+        'value' => '0',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'thumbstyle',
+        'title' => '缩略图样式',
+        'type' => 'string',
+        'content' => [],
+        'value' => '',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '用于后台列表缩略图样式,可使用:?imageView2/2/w/120/h/90/q/80',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'chunking',
+        'title' => '分片上传',
+        'type' => 'radio',
+        'content' => [
+            1 => '开启',
+            0 => '关闭',
+        ],
+        'value' => '0',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'chunksize',
+        'title' => '分片大小',
+        'type' => 'number',
+        'content' => [],
+        'value' => '4194304',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '固定大小,不能修改',
+        'ok' => '',
+        'extend' => 'readonly',
+    ],
+    [
+        'name' => 'syncdelete',
+        'title' => '附件删除时是否同步删除云存储文件',
+        'type' => 'bool',
+        'content' => [],
+        'value' => '0',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'apiupload',
+        'title' => 'API接口使用云存储',
+        'type' => 'bool',
+        'content' => [],
+        'value' => '1',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'noneedlogin',
+        'title' => '免登录上传',
+        'type' => 'checkbox',
+        'content' => [
+            'api' => 'API',
+            'index' => '前台',
+            'admin' => '后台',
+        ],
+        'value' => 'api,index,admin',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+];

+ 307 - 0
addons/qiniu/controller/Index.php

@@ -0,0 +1,307 @@
+<?php
+
+namespace addons\qiniu\controller;
+
+use app\common\exception\UploadException;
+use app\common\library\Upload;
+use app\common\model\Attachment;
+use Qiniu\Auth;
+use Qiniu\Storage\ResumeUploader;
+use Qiniu\Storage\UploadManager;
+use think\addons\Controller;
+use think\Config;
+
+/**
+ * 七牛管理
+ *
+ */
+class Index extends Controller
+{
+
+    public function _initialize()
+    {
+        //跨域检测
+        check_cors_request();
+
+        parent::_initialize();
+        Config::set('default_return_type', 'json');
+    }
+
+    public function index()
+    {
+        Config::set('default_return_type', 'html');
+        $this->error("当前插件暂无前台页面");
+    }
+
+    /**
+     * 获取签名
+     */
+    public function params()
+    {
+        $this->check();
+        $name = $this->request->post('name');
+        $md5 = $this->request->post('md5');
+        $chunk = $this->request->post('chunk');
+
+        $config = get_addon_config('qiniu');
+
+        $name = xss_clean($name);
+        $config['savekey'] = (new Upload())->getSavekey($config['savekey'], $name, '$(etag)');
+
+        preg_match('/(\d+)(\w+)/', $config['maxsize'], $matches);
+        $type = strtolower($matches[2]);
+        $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
+        $size = (int)$config['maxsize'] * pow(1024, $typeDict[$type] ?? 0);
+
+        // 构建鉴权对象
+        $auth = new Auth($config['accessKey'], $config['secretKey']);
+        // 生成上传 Token
+        $token = $auth->uploadToken($config['bucket'], null, 3600, ['saveKey' => ltrim($config['savekey'], '/'), 'fsizeLimit' => $size]);
+
+        $params['qiniutoken'] = $token;
+
+        $this->success('', null, $params);
+        return;
+    }
+
+    /**
+     * 中转上传文件
+     * 上传分片
+     * 合并分片
+     * @param bool $isApi
+     * @throws \Exception
+     */
+    public function upload($isApi = false)
+    {
+        Config::set('default_return_type', 'json');
+
+        if ($isApi === true) {
+            if (!\addons\qiniu\library\Auth::isModuleAllow()) {
+                $this->error("请登录后再进行操作");
+            }
+        } else {
+            $this->check();
+        }
+
+        $config = get_addon_config('qiniu');
+        $config['savekey'] = str_replace(
+            ['$(year)', '$(mon)', '$(day)', '$(hour)', '$(min)', '$(sec)', '$(etag)', '$(ext)', '$(fname)', '$(fprefix)'],
+            ['{year}', '{mon}', '{day}', '{hour}', '{min}', '{sec}', '{filemd5}', '{.suffix}', '{filename}', '{fileprefix}'],
+            $config['savekey']
+        );
+        $file = $this->request->file('file');
+
+        $chunkid = $this->request->post("chunkid");
+        $md5 = $chunkid && $this->request->post("action") == 'merge' ? md5(str_replace('-', '', $chunkid)) : null;
+        $config['savekey'] = (new Upload($file))->getSavekey($config['savekey'], null, $md5);
+
+        preg_match('/(\d+)(\w+)/', $config['maxsize'], $matches);
+        $type = strtolower($matches[2]);
+        $typeDict = ['b' => 0, 'k' => 1, 'kb' => 1, 'm' => 2, 'mb' => 2, 'gb' => 3, 'g' => 3];
+        $size = (int)$config['maxsize'] * pow(1024, $typeDict[$type] ?? 0);
+
+        // 构建鉴权对象
+        $auth = new Auth($config['accessKey'], $config['secretKey']);
+        // 生成上传 Token
+        $token = $auth->uploadToken($config['bucket'], null, 3600, ['saveKey' => ltrim($config['savekey'], '/'), 'fsizeLimit' => $size]);
+        // 初始化 UploadManager 对象并进行文件的上传。
+        $uploadMgr = new UploadManager();
+
+        //检测删除文件或附件
+        $checkDeleteFile = function ($attachment, $upload, $force = false) use ($config) {
+            //如果设定为不备份则删除文件和记录 或 强制删除
+            if ((isset($config['serverbackup']) && !$config['serverbackup']) || $force) {
+                if ($attachment && !empty($attachment['id'])) {
+                    $attachment->delete();
+                }
+                if ($upload) {
+                    //文件绝对路径
+                    $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
+                    @unlink($filePath);
+                }
+            }
+        };
+
+        $chunkid = $this->request->post("chunkid");
+        if ($chunkid) {
+            $action = $this->request->post("action");
+            $chunkindex = $this->request->post("chunkindex/d");
+            $chunkcount = $this->request->post("chunkcount/d");
+            $filesize = $this->request->post("filesize");
+            $filename = $this->request->post("filename");
+            if ($action == 'merge') {
+                $attachment = null;
+                $upload = null;
+                if ($config['uploadmode'] == 'server') {
+                    //合并分片文件
+                    try {
+                        $upload = new Upload();
+                        $attachment = $upload->merge($chunkid, $chunkcount, $filename);
+                    } catch (UploadException $e) {
+                        $this->error($e->getMessage());
+                    }
+                }
+
+                $config = get_addon_config('qiniu');
+
+                $name = xss_clean($filename);
+                $config['savekey'] = (new Upload())->getSavekey($config['savekey'], $name, $md5);
+
+                // 重新生成上传 Token
+                $token = $auth->uploadToken($config['bucket'], null, 3600, ['saveKey' => ltrim($config['savekey'], '/'), 'fsizeLimit' => $size]);
+
+                $contexts = $this->request->post("contexts/a", []);
+                $uploader = new ResumeUploader($token, null, null, $filesize);
+                list($ret, $err) = $uploader->setContexts($contexts)->makeFile($filename);
+                if ($err !== null) {
+                    $checkDeleteFile($attachment, $upload, true);
+                    $this->error("上传失败");
+                } else {
+                    $checkDeleteFile($attachment, $upload);
+                    $this->success("上传成功", '', ['url' => '/' . $ret['key'], 'fullurl' => cdnurl('/' . $ret['key'], true), 'hash' => $ret['hash']]);
+                }
+            } else {
+                //默认普通上传文件
+                $file = $this->request->file('file');
+                try {
+                    $upload = new Upload($file);
+                    $file = $upload->chunk($chunkid, $chunkindex, $chunkcount);
+                } catch (UploadException $e) {
+                    $this->error($e->getMessage());
+                }
+
+                //上传分片文件
+                //$file = $this->request->file('file');
+                $filesize = $file->getSize();
+                //合并分片文件
+                $uploader = new ResumeUploader($token, null, fopen($file->getRealPath(), 'rb'), $filesize);
+                $ret = $uploader->uploadChunk($chunkindex, $file, $filesize);
+                $this->success("上传成功", "", $ret);
+            }
+        } else {
+            $attachment = null;
+            //默认普通上传文件
+            $file = $this->request->file('file');
+            try {
+                $upload = new Upload($file);
+
+                $suffix = $upload->getSuffix();
+                $md5 = md5_file($file->getRealPath());
+                $filename = $file->getFilename();
+                $fileprefix = substr($filename, 0, strripos($filename, '.'));
+                $search = ['$(year)', '$(mon)', '$(day)', '$(hour)', '$(min)', '$(sec)', '$(etag)', '$(ext)', '$(fname)', '$(fprefix)'];
+                $replace = [date("Y"), date("m"), date("d"), date("H"), date("i"), date("s"), $md5, '.' . $suffix, $filename, $fileprefix];
+                $savekey = ltrim(str_replace($search, $replace, $config['savekey']), '/');
+
+                $attachment = $upload->upload($savekey);
+            } catch (UploadException $e) {
+                $this->error($e->getMessage());
+            }
+
+            //文件绝对路径
+            $filePath = $upload->getFile()->getRealPath() ?: $upload->getFile()->getPathname();
+
+            //上传到七牛后保存的文件名
+            $saveKey = ltrim($attachment->url, '/');
+
+            $url = $attachment->url;
+
+            try {
+                // 调用 UploadManager 的 putFile 方法进行文件的上传。
+                list($ret, $err) = $uploadMgr->putFile($token, $saveKey, $filePath);
+
+                if ($err !== null) {
+                    throw new \Exception("上传失败");
+                }
+                //成功不做任何操作
+            } catch (\Exception $e) {
+                $checkDeleteFile($attachment, $upload, true);
+                $this->error("上传失败");
+            }
+            $hash = md5_file($filePath);
+            $checkDeleteFile($attachment, $upload);
+
+            // 记录云存储记录
+            $data = $attachment->toArray();
+            unset($data['id']);
+            $data['storage'] = 'qiniu';
+            Attachment::create($data, true);
+
+            $this->success("上传成功", '', ['url' => $url, 'fullurl' => cdnurl($url, true), 'hash' => $hash]);
+        }
+    }
+
+    /**
+     * 通知回调
+     */
+    public function notify()
+    {
+        Config::set('default_return_type', 'json');
+
+        $this->check();
+        $config = get_addon_config('qiniu');
+        if ($config['uploadmode'] != 'client') {
+            $this->error("无需执行该操作");
+        }
+        $this->request->filter('trim,strip_tags,htmlspecialchars,xss_clean');
+
+        $size = $this->request->post('size/d');
+        $name = $this->request->post('name', '');
+        $hash = $this->request->post('hash', '');
+        $type = $this->request->post('type', '');
+        $url = $this->request->post('url', '');
+        $width = $this->request->post('width/d');
+        $height = $this->request->post('height/d');
+        $category = $this->request->post('category', '');
+        $suffix = strtolower(pathinfo($name, PATHINFO_EXTENSION));
+        $suffix = $suffix && preg_match("/^[a-zA-Z0-9]+$/", $suffix) ? $suffix : 'file';
+        $attachment = Attachment::where('url', $url)->where('storage', 'qiniu')->find();
+        if (!$attachment) {
+            $params = array(
+                'category'    => $category,
+                'admin_id'    => (int)session('admin.id'),
+                'user_id'     => (int)cookie('uid'),
+                'filename'    => $name,
+                'filesize'    => $size,
+                'imagewidth'  => $width,
+                'imageheight' => $height,
+                'imagetype'   => $suffix,
+                'imageframes' => 0,
+                'mimetype'    => $type,
+                'url'         => $url,
+                'uploadtime'  => time(),
+                'storage'     => 'qiniu',
+                'sha1'        => $hash,
+            );
+            Attachment::create($params, true);
+        }
+        $this->success();
+    }
+
+    /**
+     * 检查签名是否正确或过期
+     */
+    protected function check()
+    {
+        $qiniutoken = $this->request->post('qiniutoken', $this->request->server('AUTHORIZATION'), 'trim');
+        if (!$qiniutoken) {
+            $this->error("参数不正确(code:1)");
+        }
+        $config = get_addon_config('qiniu');
+        $auth = new Auth($config['accessKey'], $config['secretKey']);
+        list($accessKey, $sign, $data) = explode(':', $qiniutoken);
+        if (!$accessKey || !$sign || !$data) {
+            $this->error("参数不正确(code:2)");
+        }
+        if ($accessKey !== $config['accessKey']) {
+            $this->error("参数不正确(code:3)");
+        }
+        if ($accessKey . ':' . $sign !== $auth->sign($data)) {
+            $this->error("签名不正确");
+        }
+        $json = json_decode(\Qiniu\base64_urlSafeDecode($data), true);
+        if ($json['deadline'] < time()) {
+            $this->error("请求已经超时");
+        }
+    }
+}

+ 10 - 0
addons/qiniu/info.ini

@@ -0,0 +1,10 @@
+name = qiniu
+title = 七牛云储存
+intro = 使用七牛云储存,支持直传、服务器中转、分片上传
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.2.7
+state = 1
+url = /addons/qiniu
+license = regular
+licenseto = 19079

+ 37 - 0
addons/qiniu/library/Auth.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace addons\qiniu\library;
+
+class Auth
+{
+
+    public function __construct()
+    {
+
+    }
+
+    public static function isModuleAllow()
+    {
+        $config = get_addon_config('qiniu');
+        $module = request()->module();
+        $module = $module ? strtolower($module) : 'index';
+        $noNeedLogin = array_filter(explode(',', $config['noneedlogin'] ?? ''));
+        $isModuleLogin = false;
+        $tagName = 'upload_config_checklogin';
+        foreach (\think\Hook::get($tagName) as $index => $name) {
+            if (\think\Hook::exec($name, $tagName)) {
+                $isModuleLogin = true;
+                break;
+            }
+        }
+        if (in_array($module, $noNeedLogin)
+            || ($module == 'admin' && \app\admin\library\Auth::instance()->id)
+            || ($module != 'admin' && \app\common\library\Auth::instance()->id)
+            || $isModuleLogin) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+}

+ 211 - 0
addons/qiniu/library/Qiniu/Auth.php

@@ -0,0 +1,211 @@
+<?php
+namespace Qiniu;
+
+use Qiniu\Zone;
+
+final class Auth
+{
+    private $accessKey;
+    private $secretKey;
+
+    public function __construct($accessKey, $secretKey)
+    {
+        $this->accessKey = $accessKey;
+        $this->secretKey = $secretKey;
+    }
+
+    public function getAccessKey()
+    {
+        return $this->accessKey;
+    }
+
+    public function sign($data)
+    {
+        $hmac = hash_hmac('sha1', $data, $this->secretKey, true);
+        return $this->accessKey . ':' . \Qiniu\base64_urlSafeEncode($hmac);
+    }
+
+    public function signWithData($data)
+    {
+        $encodedData = \Qiniu\base64_urlSafeEncode($data);
+        return $this->sign($encodedData) . ':' . $encodedData;
+    }
+
+    public function signRequest($urlString, $body, $contentType = null)
+    {
+        $url = parse_url($urlString);
+        $data = '';
+        if (array_key_exists('path', $url)) {
+            $data = $url['path'];
+        }
+        if (array_key_exists('query', $url)) {
+            $data .= '?' . $url['query'];
+        }
+        $data .= "\n";
+
+        if ($body !== null && $contentType === 'application/x-www-form-urlencoded') {
+            $data .= $body;
+        }
+        return $this->sign($data);
+    }
+
+    public function verifyCallback($contentType, $originAuthorization, $url, $body)
+    {
+        $authorization = 'QBox ' . $this->signRequest($url, $body, $contentType);
+        return $originAuthorization === $authorization;
+    }
+
+    public function privateDownloadUrl($baseUrl, $expires = 3600)
+    {
+        $deadline = time() + $expires;
+
+        $pos = strpos($baseUrl, '?');
+        if ($pos !== false) {
+            $baseUrl .= '&e=';
+        } else {
+            $baseUrl .= '?e=';
+        }
+        $baseUrl .= $deadline;
+
+        $token = $this->sign($baseUrl);
+        return "$baseUrl&token=$token";
+    }
+
+    public function uploadToken($bucket, $key = null, $expires = 3600, $policy = null, $strictPolicy = true)
+    {
+        $deadline = time() + $expires;
+        $scope = $bucket;
+        if ($key !== null) {
+            $scope .= ':' . $key;
+        }
+
+        $args = self::copyPolicy($args, $policy, $strictPolicy);
+        $args['scope'] = $scope;
+        $args['deadline'] = $deadline;
+
+        $b = json_encode($args);
+        return $this->signWithData($b);
+    }
+
+    /**
+     *上传策略,参数规格详见
+     *http://developer.qiniu.com/docs/v6/api/reference/security/put-policy.html
+     */
+    private static $policyFields = array(
+        'callbackUrl',
+        'callbackBody',
+        'callbackHost',
+        'callbackBodyType',
+        'callbackFetchKey',
+
+        'returnUrl',
+        'returnBody',
+
+        'endUser',
+        'saveKey',
+        'insertOnly',
+
+        'detectMime',
+        'mimeLimit',
+        'fsizeMin',
+        'fsizeLimit',
+
+        'persistentOps',
+        'persistentNotifyUrl',
+        'persistentPipeline',
+
+        'deleteAfterDays',
+        'fileType',
+        'isPrefixalScope',
+    );
+
+    private static function copyPolicy(&$policy, $originPolicy, $strictPolicy)
+    {
+        if ($originPolicy === null) {
+            return array();
+        }
+        foreach ($originPolicy as $key => $value) {
+            if (!$strictPolicy || in_array((string)$key, self::$policyFields, true)) {
+                $policy[$key] = $value;
+            }
+        }
+        return $policy;
+    }
+
+    public function authorization($url, $body = null, $contentType = null)
+    {
+        $authorization = 'QBox ' . $this->signRequest($url, $body, $contentType);
+        return array('Authorization' => $authorization);
+    }
+
+    public function authorizationV2($url, $method, $body = null, $contentType = null)
+    {
+        $urlItems = parse_url($url);
+        $host = $urlItems['host'];
+
+        if (isset($urlItems['port'])) {
+            $port = $urlItems['port'];
+        } else {
+            $port = '';
+        }
+
+        $path = $urlItems['path'];
+        if (isset($urlItems['query'])) {
+            $query = $urlItems['query'];
+        } else {
+            $query = '';
+        }
+
+        //write request uri
+        $toSignStr = $method . ' ' . $path;
+        if (!empty($query)) {
+            $toSignStr .= '?' . $query;
+        }
+
+        //write host and port
+        $toSignStr .= "\nHost: " . $host;
+        if (!empty($port)) {
+            $toSignStr .= ":" . $port;
+        }
+
+        //write content type
+        if (!empty($contentType)) {
+            $toSignStr .= "\nContent-Type: " . $contentType;
+        }
+
+        $toSignStr .= "\n\n";
+
+        //write body
+        if (!empty($body)) {
+            $toSignStr .= $body;
+        }
+
+        $sign = $this->sign($toSignStr);
+        $auth = 'Qiniu ' . $sign;
+        return array('Authorization' => $auth);
+    }
+
+    public static function isModuleAllow()
+    {
+        $config = get_addon_config('qiniu');
+        $module = \addons\bos\library\request()->module();
+        $module = $module ? strtolower($module) : 'index';
+        $noNeedLogin = array_filter(explode(',', $config['noneedlogin'] ?? ''));
+        $isModuleLogin = false;
+        $tagName = 'upload_config_checklogin';
+        foreach (\think\Hook::get($tagName) as $index => $name) {
+            if (\think\Hook::exec($name, $tagName)) {
+                $isModuleLogin = true;
+                break;
+            }
+        }
+        if (in_array($module, $noNeedLogin)
+            || ($module == 'admin' && \app\admin\library\Auth::instance()->id)
+            || ($module != 'admin' && \app\common\library\Auth::instance()->id)
+            || $isModuleLogin) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+}

+ 185 - 0
addons/qiniu/library/Qiniu/Cdn/CdnManager.php

@@ -0,0 +1,185 @@
+<?php
+
+namespace Qiniu\Cdn;
+
+use Qiniu\Auth;
+use Qiniu\Http\Error;
+use Qiniu\Http\Client;
+
+final class CdnManager
+{
+
+    private $auth;
+    private $server;
+
+    public function __construct(Auth $auth)
+    {
+        $this->auth = $auth;
+        $this->server = 'http://fusion.qiniuapi.com';
+    }
+
+    /**
+     * @param array $urls 待刷新的文件链接数组
+     * @return array
+     */
+    public function refreshUrls(array $urls)
+    {
+        return $this->refreshUrlsAndDirs($urls, array());
+    }
+
+    /**
+     * @param array $dirs 待刷新的文件链接数组
+     * @return array
+     * 目前客户默认没有目录刷新权限,刷新会有400038报错,参考:https://developer.qiniu.com/fusion/api/1229/cache-refresh
+     * 需要刷新目录请工单联系技术支持 https://support.qiniu.com/tickets/category
+     */
+    public function refreshDirs(array $dirs)
+    {
+        return $this->refreshUrlsAndDirs(array(), $dirs);
+    }
+
+    /**
+     * @param array $urls 待刷新的文件链接数组
+     * @param array $dirs 待刷新的目录链接数组
+     *
+     * @return array 刷新的请求回复和错误,参考 examples/cdn_manager.php 代码
+     * @link http://developer.qiniu.com/article/fusion/api/refresh.html
+     *
+     * 目前客户默认没有目录刷新权限,刷新会有400038报错,参考:https://developer.qiniu.com/fusion/api/1229/cache-refresh
+     * 需要刷新目录请工单联系技术支持 https://support.qiniu.com/tickets/category
+     */
+    public function refreshUrlsAndDirs(array $urls, array  $dirs)
+    {
+        $req = array();
+        if (!empty($urls)) {
+            $req['urls'] = $urls;
+        }
+        if (!empty($dirs)) {
+            $req['dirs'] = $dirs;
+        }
+
+        $url = $this->server . '/v2/tune/refresh';
+        $body = json_encode($req);
+        return $this->post($url, $body);
+    }
+
+    /**
+     * @param array $urls 待预取的文件链接数组
+     *
+     * @return array 预取的请求回复和错误,参考 examples/cdn_manager.php 代码
+     *
+     * @link http://developer.qiniu.com/article/fusion/api/refresh.html
+     */
+    public function prefetchUrls(array $urls)
+    {
+        $req = array(
+            'urls' => $urls,
+        );
+
+        $url = $this->server . '/v2/tune/prefetch';
+        $body = json_encode($req);
+        return $this->post($url, $body);
+    }
+
+    /**
+     * @param array $domains 待获取带宽数据的域名数组
+     * @param string $startDate 开始的日期,格式类似 2017-01-01
+     * @param string $endDate 结束的日期,格式类似 2017-01-01
+     * @param string $granularity 获取数据的时间间隔,可以是 5min, hour 或者 day
+     *
+     * @return array 带宽数据和错误信息,参考 examples/cdn_manager.php 代码
+     *
+     * @link http://developer.qiniu.com/article/fusion/api/traffic-bandwidth.html
+     */
+    public function getBandwidthData(array $domains, $startDate, $endDate, $granularity)
+    {
+        $req = array();
+        $req['domains'] = implode(';', $domains);
+        $req['startDate'] = $startDate;
+        $req['endDate'] = $endDate;
+        $req['granularity'] = $granularity;
+
+        $url = $this->server . '/v2/tune/bandwidth';
+        $body = json_encode($req);
+        return $this->post($url, $body);
+    }
+
+    /**
+     * @param array $domains 待获取流量数据的域名数组
+     * @param string $startDate 开始的日期,格式类似 2017-01-01
+     * @param string $endDate 结束的日期,格式类似 2017-01-01
+     * @param string $granularity 获取数据的时间间隔,可以是 5min, hour 或者 day
+     *
+     * @return array 流量数据和错误信息,参考 examples/cdn_manager.php 代码
+     *
+     * @link http://developer.qiniu.com/article/fusion/api/traffic-bandwidth.html
+     */
+    public function getFluxData(array $domains, $startDate, $endDate, $granularity)
+    {
+        $req = array();
+        $req['domains'] = implode(';', $domains);
+        $req['startDate'] = $startDate;
+        $req['endDate'] = $endDate;
+        $req['granularity'] = $granularity;
+
+        $url = $this->server . '/v2/tune/flux';
+        $body = json_encode($req);
+        return $this->post($url, $body);
+    }
+
+    /**
+     * @param array $domains 待获取日志下载链接的域名数组
+     * @param string $logDate 获取指定日期的日志下载链接,格式类似 2017-01-01
+     *
+     * @return array 日志下载链接数据和错误信息,参考 examples/cdn_manager.php 代码
+     *
+     * @link http://developer.qiniu.com/article/fusion/api/log.html
+     */
+    public function getCdnLogList(array $domains, $logDate)
+    {
+        $req = array();
+        $req['domains'] = implode(';', $domains);
+        $req['day'] = $logDate;
+
+        $url = $this->server . '/v2/tune/log/list';
+        $body = json_encode($req);
+        return $this->post($url, $body);
+    }
+
+    private function post($url, $body)
+    {
+        $headers = $this->auth->authorization($url, $body, 'application/json');
+        $headers['Content-Type'] = 'application/json';
+        $ret = Client::post($url, $body, $headers);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+
+    /**
+     * 构建时间戳防盗链鉴权的访问外链
+     *
+     * @param string $rawUrl 需要签名的资源url
+     * @param string $encryptKey 时间戳防盗链密钥
+     * @param string $durationInSeconds 链接的有效期(以秒为单位)
+     *
+     * @return string 带鉴权信息的资源外链,参考 examples/cdn_timestamp_antileech.php 代码
+     */
+    public static function createTimestampAntiLeechUrl($rawUrl, $encryptKey, $durationInSeconds)
+    {
+        $parsedUrl = parse_url($rawUrl);
+        $deadline = time() + $durationInSeconds;
+        $expireHex = dechex($deadline);
+        $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
+        $strToSign = $encryptKey . $path . $expireHex;
+        $signStr = md5($strToSign);
+        if (isset($parsedUrl['query'])) {
+            $signedUrl = $rawUrl . '&sign=' . $signStr . '&t=' . $expireHex;
+        } else {
+            $signedUrl = $rawUrl . '?sign=' . $signStr . '&t=' . $expireHex;
+        }
+        return $signedUrl;
+    }
+}

+ 140 - 0
addons/qiniu/library/Qiniu/Config.php

@@ -0,0 +1,140 @@
+<?php
+namespace Qiniu;
+
+final class Config
+{
+    const SDK_VER = '7.2.10';
+
+    const BLOCK_SIZE = 4194304; //4*1024*1024 分块上传块大小,该参数为接口规格,不能修改
+
+    const RSF_HOST = 'rsf.qiniu.com';
+    const API_HOST = 'api.qiniu.com';
+    const RS_HOST = 'rs.qiniu.com';      //RS Host
+    const UC_HOST = 'uc.qbox.me';              //UC Host
+    const RTCAPI_HOST = 'http://rtc.qiniuapi.com';
+    const ARGUS_HOST = 'argus.atlab.ai';
+    const CASTER_HOST = 'pili-caster.qiniuapi.com';
+    const SMS_HOST="https://sms.qiniuapi.com";
+    const RTCAPI_VERSION = 'v3';
+    const SMS_VERSION='v1';
+
+    // Zone 空间对应的存储区域
+    public $region;
+    //BOOL 是否使用https域名
+    public $useHTTPS;
+    //BOOL 是否使用CDN加速上传域名
+    public $useCdnDomains;
+    // Zone Cache
+    private $regionCache;
+
+    // 构造函数
+    public function __construct(Region $z = null)
+    {
+        $this->zone = $z;
+        $this->useHTTPS = false;
+        $this->useCdnDomains = false;
+        $this->regionCache = array();
+    }
+
+    public function getUpHost($accessKey, $bucket)
+    {
+        $region = $this->getRegion($accessKey, $bucket);
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        $host = $region->srcUpHosts[0];
+        if ($this->useCdnDomains === true) {
+            $host = $region->cdnUpHosts[0];
+        }
+
+        return $scheme . $host;
+    }
+
+    public function getUpBackupHost($accessKey, $bucket)
+    {
+        $region = $this->getRegion($accessKey, $bucket);
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        $host = $region->cdnUpHosts[0];
+        if ($this->useCdnDomains === true) {
+            $host = $region->srcUpHosts[0];
+        }
+
+        return $scheme . $host;
+    }
+
+    public function getRsHost($accessKey, $bucket)
+    {
+        $region = $this->getRegion($accessKey, $bucket);
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return $scheme . $region->rsHost;
+    }
+
+    public function getRsfHost($accessKey, $bucket)
+    {
+        $region = $this->getRegion($accessKey, $bucket);
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return $scheme . $region->rsfHost;
+    }
+
+    public function getIovipHost($accessKey, $bucket)
+    {
+        $region = $this->getRegion($accessKey, $bucket);
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return $scheme . $region->iovipHost;
+    }
+
+    public function getApiHost($accessKey, $bucket)
+    {
+        $region = $this->getRegion($accessKey, $bucket);
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return $scheme . $region->apiHost;
+    }
+
+    private function getRegion($accessKey, $bucket)
+    {
+        $cacheId = "$accessKey:$bucket";
+
+        if (isset($this->regionCache[$cacheId])) {
+            $region = $this->regionCache[$cacheId];
+        } elseif (isset($this->zone)) {
+            $region = $this->zone;
+            $this->regionCache[$cacheId] = $region;
+        } else {
+            $region = Zone::queryZone($accessKey, $bucket);
+            $this->regionCache[$cacheId] = $region;
+        }
+        return $region;
+    }
+}

+ 76 - 0
addons/qiniu/library/Qiniu/Etag.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Qiniu;
+
+use Qiniu\Config;
+
+final class Etag
+{
+    private static function packArray($v, $a)
+    {
+        return call_user_func_array('pack', array_merge(array($v), (array)$a));
+    }
+
+    private static function blockCount($fsize)
+    {
+        return intval(($fsize + (Config::BLOCK_SIZE - 1)) / Config::BLOCK_SIZE);
+    }
+
+    private static function calcSha1($data)
+    {
+        $sha1Str = sha1($data, true);
+        $err = error_get_last();
+        if ($err !== null) {
+            return array(null, $err);
+        }
+        $byteArray = unpack('C*', $sha1Str);
+        return array($byteArray, null);
+    }
+
+
+    public static function sum($filename)
+    {
+        $fhandler = fopen($filename, 'r');
+        $err = error_get_last();
+        if ($err !== null) {
+            return array(null, $err);
+        }
+
+        $fstat = fstat($fhandler);
+        $fsize = $fstat['size'];
+        if ((int)$fsize === 0) {
+            fclose($fhandler);
+            return array('Fto5o-5ea0sNMlW_75VgGJCv2AcJ', null);
+        }
+        $blockCnt = self::blockCount($fsize);
+        $sha1Buf = array();
+
+        if ($blockCnt <= 1) {
+            array_push($sha1Buf, 0x16);
+            $fdata = fread($fhandler, Config::BLOCK_SIZE);
+            if ($err !== null) {
+                fclose($fhandler);
+                return array(null, $err);
+            }
+            list($sha1Code,) = self::calcSha1($fdata);
+            $sha1Buf = array_merge($sha1Buf, $sha1Code);
+        } else {
+            array_push($sha1Buf, 0x96);
+            $sha1BlockBuf = array();
+            for ($i = 0; $i < $blockCnt; $i++) {
+                $fdata = fread($fhandler, Config::BLOCK_SIZE);
+                list($sha1Code, $err) = self::calcSha1($fdata);
+                if ($err !== null) {
+                    fclose($fhandler);
+                    return array(null, $err);
+                }
+                $sha1BlockBuf = array_merge($sha1BlockBuf, $sha1Code);
+            }
+            $tmpData = self::packArray('C*', $sha1BlockBuf);
+            list($sha1Final,) = self::calcSha1($tmpData);
+            $sha1Buf = array_merge($sha1Buf, $sha1Final);
+        }
+        $etag = \Qiniu\base64_urlSafeEncode(self::packArray('C*', $sha1Buf));
+        return array($etag, null);
+    }
+}

+ 160 - 0
addons/qiniu/library/Qiniu/Http/Client.php

@@ -0,0 +1,160 @@
+<?php
+namespace Qiniu\Http;
+
+use Qiniu\Config;
+use Qiniu\Http\Request;
+use Qiniu\Http\Response;
+
+final class Client
+{
+    public static function get($url, array $headers = array())
+    {
+        $request = new Request('GET', $url, $headers);
+        return self::sendRequest($request);
+    }
+
+    public static function delete($url, array $headers = array())
+    {
+        $request = new Request('DELETE', $url, $headers);
+        return self::sendRequest($request);
+    }
+
+    public static function post($url, $body, array $headers = array())
+    {
+        $request = new Request('POST', $url, $headers, $body);
+        return self::sendRequest($request);
+    }
+
+    public static function PUT($url, $body, array $headers = array())
+    {
+        $request = new Request('PUT', $url, $headers, $body);
+        return self::sendRequest($request);
+    }
+
+    public static function multipartPost(
+        $url,
+        $fields,
+        $name,
+        $fileName,
+        $fileBody,
+        $mimeType = null,
+        array $headers = array()
+    ) {
+        $data = array();
+        $mimeBoundary = md5(microtime());
+
+        foreach ($fields as $key => $val) {
+            array_push($data, '--' . $mimeBoundary);
+            array_push($data, "Content-Disposition: form-data; name=\"$key\"");
+            array_push($data, '');
+            array_push($data, $val);
+        }
+
+        array_push($data, '--' . $mimeBoundary);
+        $finalMimeType = empty($mimeType) ? 'application/octet-stream' : $mimeType;
+        $finalFileName = self::escapeQuotes($fileName);
+        array_push($data, "Content-Disposition: form-data; name=\"$name\"; filename=\"$finalFileName\"");
+        array_push($data, "Content-Type: $finalMimeType");
+        array_push($data, '');
+        array_push($data, $fileBody);
+
+        array_push($data, '--' . $mimeBoundary . '--');
+        array_push($data, '');
+
+        $body = implode("\r\n", $data);
+        // var_dump($data);exit;
+        $contentType = 'multipart/form-data; boundary=' . $mimeBoundary;
+        $headers['Content-Type'] = $contentType;
+        $request = new Request('POST', $url, $headers, $body);
+        return self::sendRequest($request);
+    }
+
+    private static function userAgent()
+    {
+        $sdkInfo = "QiniuPHP/" . Config::SDK_VER;
+
+        $systemInfo = php_uname("s");
+        $machineInfo = php_uname("m");
+
+        $envInfo = "($systemInfo/$machineInfo)";
+
+        $phpVer = phpversion();
+
+        $ua = "$sdkInfo $envInfo PHP/$phpVer";
+        return $ua;
+    }
+
+    public static function sendRequest($request)
+    {
+        $t1 = microtime(true);
+        $ch = curl_init();
+        $options = array(
+            CURLOPT_USERAGENT => self::userAgent(),
+            CURLOPT_RETURNTRANSFER => true,
+            CURLOPT_SSL_VERIFYPEER => false,
+            CURLOPT_SSL_VERIFYHOST => false,
+            CURLOPT_HEADER => true,
+            CURLOPT_NOBODY => false,
+            CURLOPT_CUSTOMREQUEST => $request->method,
+            CURLOPT_URL => $request->url,
+        );
+        // Handle open_basedir & safe mode
+        if (!ini_get('safe_mode') && !ini_get('open_basedir')) {
+            $options[CURLOPT_FOLLOWLOCATION] = true;
+        }
+        if (!empty($request->headers)) {
+            $headers = array();
+            foreach ($request->headers as $key => $val) {
+                array_push($headers, "$key: $val");
+            }
+            $options[CURLOPT_HTTPHEADER] = $headers;
+        }
+        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
+        if (!empty($request->body)) {
+            $options[CURLOPT_POSTFIELDS] = $request->body;
+        }
+        curl_setopt_array($ch, $options);
+        $result = curl_exec($ch);
+        $t2 = microtime(true);
+        $duration = round($t2 - $t1, 3);
+        $ret = curl_errno($ch);
+        if ($ret !== 0) {
+            $r = new Response(-1, $duration, array(), null, curl_error($ch));
+            curl_close($ch);
+            return $r;
+        }
+        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+        $headers = self::parseHeaders(substr($result, 0, $header_size));
+        $body = substr($result, $header_size);
+        curl_close($ch);
+        return new Response($code, $duration, $headers, $body, null);
+    }
+
+    private static function parseHeaders($raw)
+    {
+        $headers = array();
+        $headerLines = explode("\r\n", $raw);
+        foreach ($headerLines as $line) {
+            $headerLine = trim($line);
+            $kv = explode(':', $headerLine);
+            if (count($kv) > 1) {
+                $kv[0] =self::ucwordsHyphen($kv[0]);
+                $headers[$kv[0]] = trim($kv[1]);
+            }
+        }
+        return $headers;
+    }
+
+    private static function escapeQuotes($str)
+    {
+        $find = array("\\", "\"");
+        $replace = array("\\\\", "\\\"");
+        return str_replace($find, $replace, $str);
+    }
+    
+    private static function ucwordsHyphen($str)
+    {
+        return str_replace('- ', '-', ucwords(str_replace('-', '- ', $str)));
+    }
+}

+ 35 - 0
addons/qiniu/library/Qiniu/Http/Error.php

@@ -0,0 +1,35 @@
+<?php
+namespace Qiniu\Http;
+
+/**
+ * 七牛业务请求逻辑错误封装类,主要用来解析API请求返回如下的内容:
+ * <pre>
+ *     {"error" : "detailed error message"}
+ * </pre>
+ */
+final class Error
+{
+    private $url;
+    private $response;
+
+    public function __construct($url, $response)
+    {
+        $this->url = $url;
+        $this->response = $response;
+    }
+
+    public function code()
+    {
+        return $this->response->statusCode;
+    }
+
+    public function getResponse()
+    {
+        return $this->response;
+    }
+
+    public function message()
+    {
+        return $this->response->error;
+    }
+}

+ 18 - 0
addons/qiniu/library/Qiniu/Http/Request.php

@@ -0,0 +1,18 @@
+<?php
+namespace Qiniu\Http;
+
+final class Request
+{
+    public $url;
+    public $headers;
+    public $body;
+    public $method;
+
+    public function __construct($method, $url, array $headers = array(), $body = null)
+    {
+        $this->method = strtoupper($method);
+        $this->url = $url;
+        $this->headers = $headers;
+        $this->body = $body;
+    }
+}

+ 176 - 0
addons/qiniu/library/Qiniu/Http/Response.php

@@ -0,0 +1,176 @@
+<?php
+
+namespace Qiniu\Http;
+
+/**
+ * HTTP response Object
+ */
+final class Response
+{
+    public $statusCode;
+    public $headers;
+    public $body;
+    public $error;
+    private $jsonData;
+    public $duration;
+
+    /** @var array Mapping of status codes to reason phrases */
+    private static $statusTexts = array(
+        100 => 'Continue',
+        101 => 'Switching Protocols',
+        102 => 'Processing',
+        200 => 'OK',
+        201 => 'Created',
+        202 => 'Accepted',
+        203 => 'Non-Authoritative Information',
+        204 => 'No Content',
+        205 => 'Reset Content',
+        206 => 'Partial Content',
+        207 => 'Multi-Status',
+        208 => 'Already Reported',
+        226 => 'IM Used',
+        300 => 'Multiple Choices',
+        301 => 'Moved Permanently',
+        302 => 'Found',
+        303 => 'See Other',
+        304 => 'Not Modified',
+        305 => 'Use Proxy',
+        307 => 'Temporary Redirect',
+        308 => 'Permanent Redirect',
+        400 => 'Bad Request',
+        401 => 'Unauthorized',
+        402 => 'Payment Required',
+        403 => 'Forbidden',
+        404 => 'Not Found',
+        405 => 'Method Not Allowed',
+        406 => 'Not Acceptable',
+        407 => 'Proxy Authentication Required',
+        408 => 'Request Timeout',
+        409 => 'Conflict',
+        410 => 'Gone',
+        411 => 'Length Required',
+        412 => 'Precondition Failed',
+        413 => 'Request Entity Too Large',
+        414 => 'Request-URI Too Long',
+        415 => 'Unsupported Media Type',
+        416 => 'Requested Range Not Satisfiable',
+        417 => 'Expectation Failed',
+        422 => 'Unprocessable Entity',
+        423 => 'Locked',
+        424 => 'Failed Dependency',
+        425 => 'Reserved for WebDAV advanced collections expired proposal',
+        426 => 'Upgrade required',
+        428 => 'Precondition Required',
+        429 => 'Too Many Requests',
+        431 => 'Request Header Fields Too Large',
+        500 => 'Internal Server Error',
+        501 => 'Not Implemented',
+        502 => 'Bad Gateway',
+        503 => 'Service Unavailable',
+        504 => 'Gateway Timeout',
+        505 => 'HTTP Version Not Supported',
+        506 => 'Variant Also Negotiates (Experimental)',
+        507 => 'Insufficient Storage',
+        508 => 'Loop Detected',
+        510 => 'Not Extended',
+        511 => 'Network Authentication Required',
+    );
+
+    /**
+     * @param int $code 状态码
+     * @param double $duration 请求时长
+     * @param array $headers 响应头部
+     * @param string $body 响应内容
+     * @param string $error 错误描述
+     */
+    public function __construct($code, $duration, array $headers = array(), $body = null, $error = null)
+    {
+        $this->statusCode = $code;
+        $this->duration = $duration;
+        $this->headers = $headers;
+        $this->body = $body;
+        $this->error = $error;
+        $this->jsonData = null;
+        if ($error !== null) {
+            return;
+        }
+
+        if ($body === null) {
+            if ($code >= 400) {
+                $this->error = self::$statusTexts[$code];
+            }
+            return;
+        }
+        if (self::isJson($headers)) {
+            try {
+                $jsonData = self::bodyJson($body);
+                if ($code >= 400) {
+                    $this->error = $body;
+                    if ($jsonData['error'] !== null) {
+                        $this->error = $jsonData['error'];
+                    }
+                }
+                $this->jsonData = $jsonData;
+            } catch (\InvalidArgumentException $e) {
+                $this->error = $body;
+                if ($code >= 200 && $code < 300) {
+                    $this->error = $e->getMessage();
+                }
+            }
+        } elseif ($code >= 400) {
+            $this->error = $body;
+        }
+        return;
+    }
+
+    public function json()
+    {
+        return $this->jsonData;
+    }
+
+    private static function bodyJson($body)
+    {
+        return \Qiniu\json_decode((string) $body, true, 512);
+    }
+
+    public function xVia()
+    {
+        $via = $this->headers['X-Via'];
+        if ($via === null) {
+            $via = $this->headers['X-Px'];
+        }
+        if ($via === null) {
+            $via = $this->headers['Fw-Via'];
+        }
+        return $via;
+    }
+
+    public function xLog()
+    {
+        return $this->headers['X-Log'];
+    }
+
+    public function xReqId()
+    {
+        return $this->headers['X-Reqid'];
+    }
+
+    public function ok()
+    {
+        return $this->statusCode >= 200 && $this->statusCode < 300 && $this->error === null;
+    }
+
+    public function needRetry()
+    {
+        $code = $this->statusCode;
+        if ($code < 0 || ($code / 100 === 5 and $code !== 579) || $code === 996) {
+            return true;
+        }
+    }
+
+    private static function isJson($headers)
+    {
+        return array_key_exists('Content-Type', $headers) &&
+        strpos($headers['Content-Type'], 'application/json') === 0;
+    }
+}

+ 282 - 0
addons/qiniu/library/Qiniu/Processing/ImageUrlBuilder.php

@@ -0,0 +1,282 @@
+<?php
+namespace Qiniu\Processing;
+
+use Qiniu;
+
+/**
+ * 主要涉及图片链接拼接
+ *
+ * @link http://developer.qiniu.com/code/v6/api/kodo-api/image/imageview2.html
+ */
+final class ImageUrlBuilder
+{
+    /**
+     * mode合法范围值
+     *
+     * @var array
+     */
+    protected $modeArr = array(0, 1, 2, 3, 4, 5);
+
+    /**
+     * format合法值
+     *
+     * @var array
+     */
+    protected $formatArr = array('psd', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'bmp');
+
+    /**
+     * 水印图片位置合法值
+     *
+     * @var array
+     */
+    protected $gravityArr = array('NorthWest', 'North', 'NorthEast',
+        'West', 'Center', 'East', 'SouthWest', 'South', 'SouthEast');
+
+    /**
+     * 缩略图链接拼接
+     *
+     * @param  string $url 图片链接
+     * @param  int $mode 缩略模式
+     * @param  int $width 宽度
+     * @param  int $height 长度
+     * @param  string $format 输出类型
+     * @param  int $quality 图片质量
+     * @param  int $interlace 是否支持渐进显示
+     * @param  int $ignoreError 忽略结果
+     * @return string
+     * @link http://developer.qiniu.com/code/v6/api/kodo-api/image/imageview2.html
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    public function thumbnail(
+        $url,
+        $mode,
+        $width,
+        $height,
+        $format = null,
+        $interlace = null,
+        $quality = null,
+        $ignoreError = 1
+    ) {
+
+        // url合法效验
+        if (!$this->isUrl($url)) {
+            return $url;
+        }
+
+        // 参数合法性效验
+        if (!in_array(intval($mode), $this->modeArr, true)) {
+            return $url;
+        }
+
+        if (!$width || !$height) {
+            return $url;
+        }
+
+        $thumbStr = 'imageView2/' . $mode . '/w/' . $width . '/h/' . $height . '/';
+
+        // 拼接输出格式
+        if (!is_null($format)
+            && in_array($format, $this->formatArr)
+        ) {
+            $thumbStr .= 'format/' . $format . '/';
+        }
+
+        // 拼接渐进显示
+        if (!is_null($interlace)
+            && in_array(intval($interlace), array(0, 1), true)
+        ) {
+            $thumbStr .= 'interlace/' . $interlace . '/';
+        }
+
+        // 拼接图片质量
+        if (!is_null($quality)
+            && intval($quality) >= 0
+            && intval($quality) <= 100
+        ) {
+            $thumbStr .= 'q/' . $quality . '/';
+        }
+
+        $thumbStr .= 'ignore-error/' . $ignoreError . '/';
+
+        // 如果有query_string用|线分割实现多参数
+        return $url . ($this->hasQuery($url) ? '|' : '?') . $thumbStr;
+    }
+
+    /**
+     * 图片水印
+     *
+     * @param  string $url 图片链接
+     * @param  string $image 水印图片链接
+     * @param  numeric $dissolve 透明度
+     * @param  string $gravity 水印位置
+     * @param  numeric $dx 横轴边距
+     * @param  numeric $dy 纵轴边距
+     * @param  numeric $watermarkScale 自适应原图的短边比例
+     * @link   http://developer.qiniu.com/code/v6/api/kodo-api/image/watermark.html
+     * @return string
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    public function waterImg(
+        $url,
+        $image,
+        $dissolve = 100,
+        $gravity = 'SouthEast',
+        $dx = null,
+        $dy = null,
+        $watermarkScale = null
+    ) {
+        // url合法效验
+        if (!$this->isUrl($url)) {
+            return $url;
+        }
+
+        $waterStr = 'watermark/1/image/' . \Qiniu\base64_urlSafeEncode($image) . '/';
+
+        // 拼接水印透明度
+        if (is_numeric($dissolve)
+            && $dissolve <= 100
+        ) {
+            $waterStr .= 'dissolve/' . $dissolve . '/';
+        }
+
+        // 拼接水印位置
+        if (in_array($gravity, $this->gravityArr, true)) {
+            $waterStr .= 'gravity/' . $gravity . '/';
+        }
+
+        // 拼接横轴边距
+        if (!is_null($dx)
+            && is_numeric($dx)
+        ) {
+            $waterStr .= 'dx/' . $dx . '/';
+        }
+
+        // 拼接纵轴边距
+        if (!is_null($dy)
+            && is_numeric($dy)
+        ) {
+            $waterStr .= 'dy/' . $dy . '/';
+        }
+
+        // 拼接自适应原图的短边比例
+        if (!is_null($watermarkScale)
+            && is_numeric($watermarkScale)
+            && $watermarkScale > 0
+            && $watermarkScale < 1
+        ) {
+            $waterStr .= 'ws/' . $watermarkScale . '/';
+        }
+
+        // 如果有query_string用|线分割实现多参数
+        return $url . ($this->hasQuery($url) ? '|' : '?') . $waterStr;
+    }
+
+    /**
+     * 文字水印
+     *
+     * @param  string $url 图片链接
+     * @param  string $text 文字
+     * @param  string $font 文字字体
+     * @param  string $fontSize 文字字号
+     * @param  string $fontColor 文字颜色
+     * @param  numeric $dissolve 透明度
+     * @param  string $gravity 水印位置
+     * @param  numeric $dx 横轴边距
+     * @param  numeric $dy 纵轴边距
+     * @link   http://developer.qiniu.com/code/v6/api/kodo-api/image/watermark.html#text-watermark
+     * @return string
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    public function waterText(
+        $url,
+        $text,
+        $font = '黑体',
+        $fontSize = 0,
+        $fontColor = null,
+        $dissolve = 100,
+        $gravity = 'SouthEast',
+        $dx = null,
+        $dy = null
+    ) {
+        // url合法效验
+        if (!$this->isUrl($url)) {
+            return $url;
+        }
+
+        $waterStr = 'watermark/2/text/'
+            . \Qiniu\base64_urlSafeEncode($text) . '/font/'
+            . \Qiniu\base64_urlSafeEncode($font) . '/';
+
+        // 拼接文字大小
+        if (is_int($fontSize)) {
+            $waterStr .= 'fontsize/' . $fontSize . '/';
+        }
+
+        // 拼接文字颜色
+        if (!is_null($fontColor)
+            && $fontColor
+        ) {
+            $waterStr .= 'fill/' . \Qiniu\base64_urlSafeEncode($fontColor) . '/';
+        }
+
+        // 拼接水印透明度
+        if (is_numeric($dissolve)
+            && $dissolve <= 100
+        ) {
+            $waterStr .= 'dissolve/' . $dissolve . '/';
+        }
+
+        // 拼接水印位置
+        if (in_array($gravity, $this->gravityArr, true)) {
+            $waterStr .= 'gravity/' . $gravity . '/';
+        }
+
+        // 拼接横轴边距
+        if (!is_null($dx)
+            && is_numeric($dx)
+        ) {
+            $waterStr .= 'dx/' . $dx . '/';
+        }
+
+        // 拼接纵轴边距
+        if (!is_null($dy)
+            && is_numeric($dy)
+        ) {
+            $waterStr .= 'dy/' . $dy . '/';
+        }
+
+        // 如果有query_string用|线分割实现多参数
+        return $url . ($this->hasQuery($url) ? '|' : '?') . $waterStr;
+    }
+
+    /**
+     * 效验url合法性
+     *
+     * @param  string $url url链接
+     * @return string
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    protected function isUrl($url)
+    {
+        $urlArr = parse_url($url);
+
+        return $urlArr['scheme']
+        && in_array($urlArr['scheme'], array('http', 'https'))
+        && $urlArr['host']
+        && $urlArr['path'];
+    }
+
+    /**
+     * 检测是否有query
+     *
+     * @param  string $url url链接
+     * @return string
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    protected function hasQuery($url)
+    {
+        $urlArr = parse_url($url);
+
+        return !empty($urlArr['query']);
+    }
+}

+ 60 - 0
addons/qiniu/library/Qiniu/Processing/Operation.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Qiniu\Processing;
+
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+
+final class Operation
+{
+
+    private $auth;
+    private $token_expire;
+    private $domain;
+
+    public function __construct($domain, $auth = null, $token_expire = 3600)
+    {
+        $this->auth = $auth;
+        $this->domain = $domain;
+        $this->token_expire = $token_expire;
+    }
+
+
+    /**
+     * 对资源文件进行处理
+     *
+     * @param $key   待处理的资源文件名
+     * @param $fops   string|array  fop操作,多次fop操作以array的形式传入。
+     *                eg. imageView2/1/w/200/h/200, imageMogr2/thumbnail/!75px
+     *
+     * @return array 文件处理后的结果及错误。
+     *
+     * @link http://developer.qiniu.com/docs/v6/api/reference/fop/
+     */
+    public function execute($key, $fops)
+    {
+        $url = $this->buildUrl($key, $fops);
+        $resp = Client::get($url);
+        if (!$resp->ok()) {
+            return array(null, new Error($url, $resp));
+        }
+        if ($resp->json() !== null) {
+            return array($resp->json(), null);
+        }
+        return array($resp->body, null);
+    }
+
+    public function buildUrl($key, $fops, $protocol = 'http')
+    {
+        if (is_array($fops)) {
+            $fops = implode('|', $fops);
+        }
+
+        $url = $protocol . "://$this->domain/$key?$fops";
+        if ($this->auth !== null) {
+            $url = $this->auth->privateDownloadUrl($url, $this->token_expire);
+        }
+
+        return $url;
+    }
+}

+ 94 - 0
addons/qiniu/library/Qiniu/Processing/PersistentFop.php

@@ -0,0 +1,94 @@
+<?php
+namespace Qiniu\Processing;
+
+use Qiniu\Config;
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+use Qiniu\Processing\Operation;
+
+/**
+ * 持久化处理类,该类用于主动触发异步持久化操作.
+ *
+ * @link http://developer.qiniu.com/docs/v6/api/reference/fop/pfop/pfop.html
+ */
+final class PersistentFop
+{
+    /**
+     * @var 账号管理密钥对,Auth对象
+     */
+    private $auth;
+
+    /*
+     * @var 配置对象,Config 对象
+     * */
+    private $config;
+
+
+    public function __construct($auth, $config = null)
+    {
+        $this->auth = $auth;
+        if ($config == null) {
+            $this->config = new Config();
+        } else {
+            $this->config = $config;
+        }
+    }
+
+    /**
+     * 对资源文件进行异步持久化处理
+     * @param $bucket     资源所在空间
+     * @param $key        待处理的源文件
+     * @param $fops       string|array  待处理的pfop操作,多个pfop操作以array的形式传入。
+     *                    eg. avthumb/mp3/ab/192k, vframe/jpg/offset/7/w/480/h/360
+     * @param $pipeline   资源处理队列
+     * @param $notify_url 处理结果通知地址
+     * @param $force      是否强制执行一次新的指令
+     *
+     *
+     * @return array 返回持久化处理的persistentId, 和返回的错误。
+     *
+     * @link http://developer.qiniu.com/docs/v6/api/reference/fop/
+     */
+    public function execute($bucket, $key, $fops, $pipeline = null, $notify_url = null, $force = false)
+    {
+        if (is_array($fops)) {
+            $fops = implode(';', $fops);
+        }
+        $params = array('bucket' => $bucket, 'key' => $key, 'fops' => $fops);
+        \Qiniu\setWithoutEmpty($params, 'pipeline', $pipeline);
+        \Qiniu\setWithoutEmpty($params, 'notifyURL', $notify_url);
+        if ($force) {
+            $params['force'] = 1;
+        }
+        $data = http_build_query($params);
+        $scheme = "http://";
+        if ($this->config->useHTTPS === true) {
+            $scheme = "https://";
+        }
+        $url = $scheme . Config::API_HOST . '/pfop/';
+        $headers = $this->auth->authorization($url, $data, 'application/x-www-form-urlencoded');
+        $headers['Content-Type'] = 'application/x-www-form-urlencoded';
+        $response = Client::post($url, $data, $headers);
+        if (!$response->ok()) {
+            return array(null, new Error($url, $response));
+        }
+        $r = $response->json();
+        $id = $r['persistentId'];
+        return array($id, null);
+    }
+
+    public function status($id)
+    {
+        $scheme = "http://";
+
+        if ($this->config->useHTTPS === true) {
+            $scheme = "https://";
+        }
+        $url = $scheme . Config::API_HOST . "/status/get/prefop?id=$id";
+        $response = Client::get($url);
+        if (!$response->ok()) {
+            return array(null, new Error($url, $response));
+        }
+        return array($response->json(), null);
+    }
+}

+ 196 - 0
addons/qiniu/library/Qiniu/Region.php

@@ -0,0 +1,196 @@
+<?php
+namespace Qiniu;
+
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+
+class Region
+{
+
+    //源站上传域名
+    public $srcUpHosts;
+    //CDN加速上传域名
+    public $cdnUpHosts;
+    //资源管理域名
+    public $rsHost;
+    //资源列举域名
+    public $rsfHost;
+    //资源处理域名
+    public $apiHost;
+    //IOVIP域名
+    public $iovipHost;
+
+    //构造一个Region对象
+    public function __construct(
+        $srcUpHosts = array(),
+        $cdnUpHosts = array(),
+        $rsHost = "rs.qiniu.com",
+        $rsfHost = "rsf.qiniu.com",
+        $apiHost = "api.qiniu.com",
+        $iovipHost = null
+    ) {
+
+        $this->srcUpHosts = $srcUpHosts;
+        $this->cdnUpHosts = $cdnUpHosts;
+        $this->rsHost = $rsHost;
+        $this->rsfHost = $rsfHost;
+        $this->apiHost = $apiHost;
+        $this->iovipHost = $iovipHost;
+    }
+
+    //华东机房
+    public static function regionHuadong()
+    {
+        $regionHuadong = new Region(
+            array("up.qiniup.com", 'up-jjh.qiniup.com', 'up-xs.qiniup.com'),
+            array('upload.qiniup.com', 'upload-jjh.qiniup.com', 'upload-xs.qiniup.com'),
+            'rs.qbox.me',
+            'rsf.qbox.me',
+            'api.qiniu.com',
+            'iovip.qbox.me'
+        );
+        return $regionHuadong;
+    }
+
+    //华东机房内网上传
+    public static function qvmRegionHuadong()
+    {
+        $qvmRegionHuadong = new Region(
+            array("free-qvm-z0-xs.qiniup.com"),
+            'rs.qbox.me',
+            'rsf.qbox.me',
+            'api.qiniu.com',
+            'iovip.qbox.me'
+        );
+        return $qvmRegionHuadong;
+    }
+
+    //华北机房内网上传
+    public static function qvmRegionHuabei()
+    {
+        $qvmRegionHuabei = new Region(
+            array("free-qvm-z1-zz.qiniup.com"),
+            "rs-z1.qbox.me",
+            "rsf-z1.qbox.me",
+            "api-z1.qiniu.com",
+            "iovip-z1.qbox.me"
+        );
+        return $qvmRegionHuabei;
+    }
+
+    //华北机房
+    public static function regionHuabei()
+    {
+        $regionHuabei = new Region(
+            array('up-z1.qiniup.com'),
+            array('upload-z1.qiniup.com'),
+            "rs-z1.qbox.me",
+            "rsf-z1.qbox.me",
+            "api-z1.qiniu.com",
+            "iovip-z1.qbox.me"
+        );
+
+        return $regionHuabei;
+    }
+
+    //华南机房
+    public static function regionHuanan()
+    {
+        $regionHuanan = new Region(
+            array('up-z2.qiniup.com', 'up-dg.qiniup.com', 'up-fs.qiniup.com'),
+            array('upload-z2.qiniup.com', 'upload-dg.qiniup.com', 'upload-fs.qiniup.com'),
+            "rs-z2.qbox.me",
+            "rsf-z2.qbox.me",
+            "api-z2.qiniu.com",
+            "iovip-z2.qbox.me"
+        );
+        return $regionHuanan;
+    }
+
+    //北美机房
+    public static function regionNorthAmerica()
+    {
+        //北美机房
+        $regionNorthAmerica = new Region(
+            array('up-na0.qiniup.com'),
+            array('upload-na0.qiniup.com'),
+            "rs-na0.qbox.me",
+            "rsf-na0.qbox.me",
+            "api-na0.qiniu.com",
+            "iovip-na0.qbox.me"
+        );
+        return $regionNorthAmerica;
+    }
+
+    //新加坡机房
+    public static function regionSingapore()
+    {
+        //新加坡机房
+        $regionSingapore = new Region(
+            array('up-as0.qiniup.com'),
+            array('upload-as0.qiniup.com'),
+            "rs-as0.qbox.me",
+            "rsf-as0.qbox.me",
+            "api-as0.qiniu.com",
+            "iovip-as0.qbox.me"
+        );
+        return $regionSingapore;
+    }
+
+    /*
+     * GET /v2/query?ak=<ak>&&bucket=<bucket>
+     **/
+    public static function queryRegion($ak, $bucket)
+    {
+        $Region = new Region();
+        $url = Config::API_HOST . '/v2/query' . "?ak=$ak&bucket=$bucket";
+        $ret = Client::Get($url);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        //parse Region;
+
+        $iovipHost = $r['io']['src']['main'][0];
+        $Region->iovipHost = $iovipHost;
+        $accMain = $r['up']['acc']['main'][0];
+        array_push($Region->cdnUpHosts, $accMain);
+        if (isset($r['up']['acc']['backup'])) {
+            foreach ($r['up']['acc']['backup'] as $key => $value) {
+                array_push($Region->cdnUpHosts, $value);
+            }
+        }
+        $srcMain = $r['up']['src']['main'][0];
+        array_push($Region->srcUpHosts, $srcMain);
+        if (isset($r['up']['src']['backup'])) {
+            foreach ($r['up']['src']['backup'] as $key => $value) {
+                array_push($Region->srcUpHosts, $value);
+            }
+        }
+
+        //set specific hosts
+        if (strstr($Region->iovipHost, "z1") !== false) {
+            $Region->rsHost = "rs-z1.qbox.me";
+            $Region->rsfHost = "rsf-z1.qbox.me";
+            $Region->apiHost = "api-z1.qiniu.com";
+        } elseif (strstr($Region->iovipHost, "z2") !== false) {
+            $Region->rsHost = "rs-z2.qbox.me";
+            $Region->rsfHost = "rsf-z2.qbox.me";
+            $Region->apiHost = "api-z2.qiniu.com";
+        } elseif (strstr($Region->iovipHost, "na0") !== false) {
+            $Region->rsHost = "rs-na0.qbox.me";
+            $Region->rsfHost = "rsf-na0.qbox.me";
+            $Region->apiHost = "api-na0.qiniu.com";
+        } elseif (strstr($Region->iovipHost, "as0") !== false) {
+            $Region->rsHost = "rs-as0.qbox.me";
+            $Region->rsfHost = "rsf-as0.qbox.me";
+            $Region->apiHost = "api-as0.qiniu.com";
+        } else {
+            $Region->rsHost = "rs.qbox.me";
+            $Region->rsfHost = "rsf.qbox.me";
+            $Region->apiHost = "api.qiniu.com";
+        }
+
+        return $Region;
+    }
+}

+ 209 - 0
addons/qiniu/library/Qiniu/Rtc/AppClient.php

@@ -0,0 +1,209 @@
+<?php
+namespace Qiniu\Rtc;
+
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+use Qiniu\Config;
+use Qiniu\Auth;
+
+class AppClient
+{
+    private $auth;
+    private $baseURL;
+
+    public function __construct(Auth $auth)
+    {
+        $this->auth = $auth;
+
+        $this->baseURL = sprintf("%s/%s/apps", Config::RTCAPI_HOST, Config::RTCAPI_VERSION);
+    }
+
+    /*
+     * 创建应用
+     * hub: 直播空间名
+     * title: app 的名称  注意,Title 不是唯一标识,重复 create 动作将生成多个 app
+     * maxUsers:人数限制
+     * NoAutoKickUser: bool 类型,可选,禁止自动踢人(抢流)。默认为 false ,
+       即同一个身份的 client (app/room/user) ,新的连麦请求可以成功,旧连接被关闭。
+     */
+    public function createApp($hub, $title, $maxUsers = null, $noAutoKickUser = null)
+    {
+        $params['hub'] = $hub;
+        $params['title'] = $title;
+        if (!empty($maxUsers)) {
+            $params['maxUsers'] = $maxUsers;
+        }
+        if ($noAutoKickUser !== null) {
+            $params['noAutoKickUser'] = $noAutoKickUser;
+        }
+        $body = json_encode($params);
+        $ret = $this->post($this->baseURL, $body);
+        return $ret;
+    }
+
+    /*
+     * 更新应用
+     * appId: app 的唯一标识,创建的时候由系统生成。
+     * Title: app 的名称, 可选。
+     * Hub: 绑定的直播 hub,可选,用于合流后 rtmp 推流。
+     * MaxUsers: int 类型,可选,连麦房间支持的最大在线人数。
+     * NoAutoKickUser: bool 类型,可选,禁止自动踢人。
+     * MergePublishRtmp: 连麦合流转推 RTMP 的配置,可选择。其详细配置包括如下
+            Enable: 布尔类型,用于开启和关闭所有房间的合流功能。
+            AudioOnly: 布尔类型,可选,指定是否只合成音频。
+            Height, Width: int64,可选,指定合流输出的高和宽,默认为 640 x 480。
+            OutputFps: int64,可选,指定合流输出的帧率,默认为 25 fps 。
+            OutputKbps: int64,可选,指定合流输出的码率,默认为 1000 。
+            URL: 合流后转推旁路直播的地址,可选,支持魔法变量配置按照连麦房间号生成不同的推流地址。如果是转推到七牛直播云,不建议使用该配置。
+            StreamTitle: 转推七牛直播云的流名,可选,支持魔法变量配置按照连麦房间号生成不同的流名。例如,配置 Hub 为 qn-zhibo ,配置 StreamTitle 为 $(roomName) ,则房间 meeting-001 的合流将会被转推到 rtmp://pili-publish.qn-zhibo.***.com/qn-zhibo/meeting-001地址。详细配置细则,请咨询七牛技术支持。
+     */
+    public function updateApp($appId, $hub, $title, $maxUsers = null, $mergePublishRtmp = null, $noAutoKickUser = null)
+    {
+        $url = $this->baseURL . '/' . $appId;
+        $params['hub'] = $hub;
+        $params['title'] = $title;
+        if (!empty($maxUsers)) {
+            $params['maxUsers'] = $maxUsers;
+        }
+        if ($noAutoKickUser !== null) {
+            $params['noAutoKickUser'] = $noAutoKickUser;
+        }
+        if (!empty($mergePublishRtmp)) {
+            $params['mergePublishRtmp'] = $mergePublishRtmp;
+        }
+        $body = json_encode($params);
+        $ret = $this->post($url, $body);
+        return $ret;
+    }
+
+    /*
+     * 获取应用信息
+     * appId: app 的唯一标识,创建的时候由系统生成。
+     */
+    public function getApp($appId)
+    {
+        $url = $this->baseURL . '/' . $appId;
+        $ret  = $this->get($url);
+        return $ret;
+    }
+
+    /*
+     * 删除应用
+     * appId: app 的唯一标识,创建的时候由系统生成
+     */
+    public function deleteApp($appId)
+    {
+        $url = $this->baseURL . '/' . $appId;
+        list(, $err)  = $this->delete($url);
+        return $err;
+    }
+
+    /*
+     * 获取房间内用户列表
+     * appId: app 的唯一标识,创建的时候由系统生成。
+     * roomName: 操作所查询的连麦房间。
+     */
+    public function listUser($appId, $roomName)
+    {
+        $url = sprintf("%s/%s/rooms/%s/users", $this->baseURL, $appId, $roomName);
+        $ret  = $this->get($url);
+        return $ret;
+    }
+
+   /*
+    * 踢出用户
+    * appId: app 的唯一标识,创建的时候由系统生成。
+    * roomName: 连麦房间
+    * userId: 请求加入房间的用户ID
+    */
+    public function kickUser($appId, $roomName, $userId)
+    {
+        $url = sprintf("%s/%s/rooms/%s/users/%s", $this->baseURL, $appId, $roomName, $userId);
+        list(, $err)  = $this->delete($url);
+        return $err;
+    }
+
+    /*
+     * 获取应用中活跃房间
+     * appId: app 的唯一标识,创建的时候由系统生成。
+     * prefix: 所查询房间名的前缀索引,可以为空。
+     * offset: int 类型,分页查询的位移标记。
+     * limit: int 类型,此次查询的最大长度。
+     * GET /v3/apps/<AppID>/rooms?prefix=<RoomNamePrefix>&offset=<Offset>&limit=<Limit>
+     */
+    public function listActiveRooms($appId, $prefix = null, $offset = null, $limit = null)
+    {
+        if (isset($prefix)) {
+            $query['prefix'] = $prefix;
+        }
+        if (isset($offset)) {
+            $query['offset'] = $offset;
+        }
+        if (isset($limit)) {
+            $query['limit'] = $limit;
+        }
+        if (isset($query) && !empty($query)) {
+            $query = '?' . http_build_query($query);
+            $url = sprintf("%s/%s/rooms%s", $this->baseURL, $appId, $query);
+        } else {
+            $url = sprintf("%s/%s/rooms", $this->baseURL, $appId);
+        }
+        $ret  = $this->get($url);
+        return $ret;
+    }
+
+    /*
+     * 生成加入房间的令牌
+     * appId: app 的唯一标识,创建的时候由系统生成。
+     * roomName: 房间名称,需满足规格 ^[a-zA-Z0-9_-]{3,64}$
+     * userId: 请求加入房间的用户 ID,需满足规格 ^[a-zA-Z0-9_-]{3,50}$
+     * expireAt: int64 类型,鉴权的有效时间,传入以秒为单位的64位Unix
+       绝对时间,token 将在该时间后失效。
+     * permission: 该用户的房间管理权限,"admin" 或 "user",默认为 "user" 。
+       当权限角色为 "admin" 时,拥有将其他用户移除出房间等特权.
+     */
+    public function appToken($appId, $roomName, $userId, $expireAt, $permission)
+    {
+        $params['appId'] = $appId;
+        $params['userId'] = $userId;
+        $params['roomName'] = $roomName;
+        $params['permission'] = $permission;
+        $params['expireAt'] = $expireAt;
+        $appAccessString = json_encode($params);
+        return $this->auth->signWithData($appAccessString);
+    }
+
+    private function get($url, $cType = null)
+    {
+        $rtcToken = $this->auth->authorizationV2($url, "GET", null, $cType);
+        $rtcToken['Content-Type'] = $cType;
+        $ret = Client::get($url, $rtcToken);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        return array($ret->json(), null);
+    }
+
+    private function delete($url, $contentType = 'application/json')
+    {
+        $rtcToken = $this->auth->authorizationV2($url, "DELETE", null, $contentType);
+        $rtcToken['Content-Type'] = $contentType;
+        $ret = Client::delete($url, $rtcToken);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        return array($ret->json(), null);
+    }
+
+    private function post($url, $body, $contentType = 'application/json')
+    {
+        $rtcToken = $this->auth->authorizationV2($url, "POST", $body, $contentType);
+        $rtcToken['Content-Type'] = $contentType;
+        $ret = Client::post($url, $body, $rtcToken);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+}

+ 337 - 0
addons/qiniu/library/Qiniu/Sms/Sms.php

@@ -0,0 +1,337 @@
+<?php
+namespace Qiniu\Sms;
+
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+use Qiniu\Config;
+use Qiniu\Auth;
+
+class Sms
+{
+    private $auth;
+    private $baseURL;
+
+    public function __construct(Auth $auth)
+    {
+        $this->auth = $auth;
+
+        $this->baseURL = sprintf("%s/%s/", Config::SMS_HOST, Config::SMS_VERSION);
+    }
+
+    /*
+     * 创建签名
+     * signature: string 类型,必填,【长度限制8个字符内】超过长度会报错
+     * source: string   类型,必填,申请签名时必须指定签名来源。取值范围为:
+        nterprises_and_institutions 企事业单位的全称或简称
+        website 工信部备案网站的全称或简称
+        app APP应用的全称或简称
+        public_number_or_small_program 公众号或小程序的全称或简称
+        store_name 电商平台店铺名的全称或简称
+        trade_name 商标名的全称或简称,
+     * pics: 本地的图片路径 string 类型,可选
+     *@return: 类型array {
+        "signature_id": <signature_id>
+        }
+     */
+    public function createSignature($signature, $source, $pics = null)
+    {
+        $params['signature'] = $signature;
+        $params['source'] = $source;
+        if (!empty($pics)) {
+            $params['pics'] = $this->imgToBase64($pics);
+        }
+        $body = json_encode($params);
+        $url =$this->baseURL.'signature';
+        $ret = $this->post($url, $body);
+        return $ret;
+    }
+
+    /*
+    * 编辑签名
+    *  id 签名id : string 类型,必填,
+    * signature: string 类型,必填,
+    * source: string    类型,必填,申请签名时必须指定签名来源。取值范围为:
+        enterprises_and_institutions 企事业单位的全称或简称
+        website 工信部备案网站的全称或简称
+        app APP应用的全称或简称
+        public_number_or_small_program 公众号或小程序的全称或简称
+        store_name 电商平台店铺名的全称或简称
+        trade_name 商标名的全称或简称,
+    * pics: 本地的图片路径 string   类型,可选,
+    * @return: 类型array {
+        "signature": string
+        }
+    */
+    public function updateSignature($id, $signature, $source, $pics = null)
+    {
+        $params['signature'] = $signature;
+        $params['source'] = $source;
+        if (!empty($pics)) {
+            $params['pics'] = $this->imgToBase64($pics);
+        }
+        $body = json_encode($params);
+        $url =$this->baseURL.'signature/'.$id;
+        $ret = $this->PUT($url, $body);
+        return $ret;
+    }
+
+    /*
+ * 查询签名
+ * audit_status: 审核状态 string 类型,可选,
+   取值范围为: "passed"(通过), "rejected"(未通过), "reviewing"(审核中)
+ * page:页码 int  类型,
+ * page_size: 分页大小 int 类型,可选, 默认为20
+ *@return: 类型array {
+    "items": [{
+        "id": string,
+        "signature": string,
+        "source": string,
+        "audit_status": string,
+        "reject_reason": string,
+        "created_at": int64,
+        "updated_at": int64
+            }...],
+    "total": int,
+    "page": int,
+    "page_size": int,
+    }
+ */
+    public function checkSignature($audit_status = null, $page = 1, $page_size = 20)
+    {
+
+        $url = sprintf(
+            "%s?audit_status=%s&page=%s&page_size=%s",
+            $this->baseURL.'signature',
+            $audit_status,
+            $page,
+            $page_size
+        );
+        $ret  = $this->get($url);
+        return $ret;
+    }
+
+
+    /*
+ * 删除签名
+ * id 签名id string 类型,必填,
+ * @retrun : 请求成功 HTTP 状态码为 200
+ */
+    public function deleteSignature($id)
+    {
+        $url = $this->baseURL . 'signature/' . $id;
+        list(, $err)  = $this->delete($url);
+        return $err;
+    }
+
+
+
+
+    /*
+    * 创建模板
+    * name  : 模板名称 string 类型 ,必填
+    * template:  模板内容 string  类型,必填
+    * type: 模板类型 string 类型,必填,
+      取值范围为: notification (通知类短信), verification (验证码短信), marketing (营销类短信)
+    * description:  申请理由简述 string  类型,必填
+    * signature_id:  已经审核通过的签名 string  类型,必填
+    * @return: 类型 array {
+        "template_id": string
+                }
+    */
+    public function createTemplate(
+        $name,
+        $template,
+        $type,
+        $description,
+        $signture_id
+    ) {
+        $params['name'] = $name;
+        $params['template'] = $template;
+        $params['type'] = $type;
+        $params['description'] = $description;
+        $params['signature_id'] = $signture_id;
+
+        $body = json_encode($params);
+        $url =$this->baseURL.'template';
+        $ret = $this->post($url, $body);
+        return $ret;
+    }
+
+    /*
+  * 查询模板
+  * audit_status: 审核状态 string 类型 ,可选,
+    取值范围为: passed (通过), rejected (未通过), reviewing (审核中)
+  * page:  页码 int  类型,可选,默认为 1
+  * page_size: 分页大小 int 类型,可选,默认为 20
+  * @return: 类型array{
+      "items": [{
+            "id": string,
+            "name": string,
+            "template": string,
+            "audit_status": string,
+            "reject_reason": string,
+            "type": string,
+            "signature_id": string, // 模版绑定的签名ID
+            "signature_text": string, // 模版绑定的签名内容
+            "created_at": int64,
+            "updated_at": int64
+        }...],
+        "total": int,
+        "page": int,
+        "page_size": int
+        }
+  */
+    public function queryTemplate($audit_status = null, $page = 1, $page_size = 20)
+    {
+
+        $url = sprintf(
+            "%s?audit_status=%s&page=%s&page_size=%s",
+            $this->baseURL.'template',
+            $audit_status,
+            $page,
+            $page_size
+        );
+        $ret  = $this->get($url);
+        return $ret;
+    }
+
+    /*
+    * 编辑模板
+    * id :模板id
+    * name  : 模板名称 string 类型 ,必填
+    * template:  模板内容 string  类型,必填
+    * description:  申请理由简述 string  类型,必填
+    * signature_id:  已经审核通过的签名 string  类型,必填
+    * @retrun : 请求成功 HTTP 状态码为 200
+    */
+    public function updateTemplate(
+        $id,
+        $name,
+        $template,
+        $description,
+        $signature_id
+    ) {
+        $params['name'] = $name;
+        $params['template'] = $template;
+        $params['description'] = $description;
+        $params['signature_id'] = $signature_id;
+        $body = json_encode($params);
+        $url =$this->baseURL.'template/'.$id;
+        $ret = $this->PUT($url, $body);
+        return $ret;
+    }
+
+    /*
+    * 删除模板
+    * id :模板id string 类型,必填,
+    * @retrun : 请求成功 HTTP 状态码为 200
+    */
+    public function deleteTemplate($id)
+    {
+        $url = $this->baseURL . 'template/' . $id;
+        list(, $err)  = $this->delete($url);
+        return $err;
+    }
+
+    /*
+    * 发送短信
+    * 编辑模板
+    * template_id :模板id string类型,必填
+    * mobiles   : 手机号数组 []string 类型 ,必填
+    * parameters:  模板内容 map[string]string     类型,可选
+    * @return: 类型json {
+        "job_id": string
+        }
+    */
+    public function sendMessage($template_id, $mobiles, $parameters = null)
+    {
+        $params['template_id'] = $template_id;
+        $params['mobiles'] = $mobiles;
+        if (!empty($parameters)) {
+            $params['parameters'] = $parameters;
+        }
+        $body = json_encode($params);
+        $url =$this->baseURL.'message';
+        $ret = $this->post($url, $body);
+        return $ret;
+    }
+
+    public function imgToBase64($img_file)
+    {
+        $img_base64 = '';
+        if (file_exists($img_file)) {
+            $app_img_file = $img_file; // 图片路径
+            $img_info = getimagesize($app_img_file); // 取得图片的大小,类型等
+            $fp = fopen($app_img_file, "r"); // 图片是否可读权限
+            if ($fp) {
+                $filesize = filesize($app_img_file);
+                if ($filesize > 5*1024*1024) {
+                    die("pic size < 5M !");
+                }
+                $content = fread($fp, $filesize);
+                $file_content = chunk_split(base64_encode($content)); // base64编码
+                switch ($img_info[2]) {           //判读图片类型
+                    case 1:
+                        $img_type = 'gif';
+                        break;
+                    case 2:
+                        $img_type = 'jpg';
+                        break;
+                    case 3:
+                        $img_type = 'png';
+                        break;
+                }
+                //合成图片的base64编码
+                $img_base64 = 'data:image/' . $img_type . ';base64,' . $file_content;
+            }
+            fclose($fp);
+        }
+
+        return $img_base64;
+    }
+
+    private function get($url, $cType = null)
+    {
+        $rtcToken = $this->auth->authorizationV2($url, "GET", null, $cType);
+        $rtcToken['Content-Type'] = $cType;
+        $ret = Client::get($url, $rtcToken);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        return array($ret->json(), null);
+    }
+
+    private function delete($url, $contentType = 'application/json')
+    {
+        $rtcToken = $this->auth->authorizationV2($url, "DELETE", null, $contentType);
+        $rtcToken['Content-Type'] = $contentType;
+        $ret = Client::delete($url, $rtcToken);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        return array($ret->json(), null);
+    }
+
+    private function post($url, $body, $contentType = 'application/json')
+    {
+        $rtcToken = $this->auth->authorizationV2($url, "POST", $body, $contentType);
+        $rtcToken['Content-Type'] = $contentType;
+        $ret = Client::post($url, $body, $rtcToken);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+    private function PUT($url, $body, $contentType = 'application/json')
+    {
+        $rtcToken = $this->auth->authorizationV2($url, "PUT", $body, $contentType);
+        $rtcToken['Content-Type'] = $contentType;
+        $ret = Client::put($url, $body, $rtcToken);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+}

+ 73 - 0
addons/qiniu/library/Qiniu/Storage/ArgusManager.php

@@ -0,0 +1,73 @@
+<?php
+namespace Qiniu\Storage;
+
+use Qiniu\Auth;
+use Qiniu\Config;
+use Qiniu\Zone;
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+
+/**
+ * 主要涉及了鉴黄接口的实现,具体的接口规格可以参考
+ *
+ * @link https://developer.qiniu.com/dora/manual/3674/kodo-product-introduction
+ */
+final class ArgusManager
+{
+    private $auth;
+    private $config;
+
+    public function __construct(Auth $auth, Config $config = null)
+    {
+        $this->auth = $auth;
+        if ($config == null) {
+            $this->config = new Config();
+        } else {
+            $this->config = $config;
+        }
+    }
+
+    /**
+     * 视频鉴黄
+     *
+     * @param $body     body信息
+     * @param $vid      videoID
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  https://developer.qiniu.com/dora/manual/4258/video-pulp
+     */
+    public function pulpVideo($body, $vid)
+    {
+        $path = '/v1/video/' . $vid;
+        
+        return $this->arPost($path, $body);
+    }
+
+    private function getArHost()
+    {
+        $scheme = "http://";
+        if ($this->config->useHTTPS == true) {
+            $scheme = "https://";
+        }
+        return $scheme . Config::ARGUS_HOST;
+    }
+
+    private function arPost($path, $body = null)
+    {
+        $url = $this->getArHost() . $path;
+        return $this->post($url, $body);
+    }
+
+    private function post($url, $body)
+    {
+        $headers = $this->auth->authorizationV2($url, 'POST', $body, 'application/json');
+        $headers['Content-Type']='application/json';
+        $ret = Client::post($url, $body, $headers);
+        if (!$ret->ok()) {
+            print($ret->statusCode);
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+}

+ 1107 - 0
addons/qiniu/library/Qiniu/Storage/BucketManager.php

@@ -0,0 +1,1107 @@
+<?php
+namespace Qiniu\Storage;
+
+use Qiniu\Auth;
+use Qiniu\Config;
+use Qiniu\Zone;
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+
+/**
+ * 主要涉及了空间资源管理及批量操作接口的实现,具体的接口规格可以参考
+ *
+ * @link https://developer.qiniu.com/kodo/api/1274/rs
+ */
+final class BucketManager
+{
+    private $auth;
+    private $config;
+
+    public function __construct(Auth $auth, Config $config = null)
+    {
+        $this->auth = $auth;
+        if ($config == null) {
+            $this->config = new Config();
+        } else {
+            $this->config = $config;
+        }
+    }
+
+    /**
+     * 获取指定账号下所有的空间名。
+     *
+     * @return string[] 包含所有空间名
+     */
+    public function buckets($shared = true)
+    {
+        $includeShared = "false";
+        if ($shared === true) {
+            $includeShared = "true";
+        }
+        return $this->rsGet('/buckets?shared=' . $includeShared);
+    }
+
+    /**
+     * 列举空间,返回bucket列表
+     * region 指定区域,global 指定全局空间。
+     * 在指定了 region 参数时,
+     * 如果指定 global 为 true,那么忽略 region 参数指定的区域,返回所有区域的全局空间。
+     * 如果没有指定 global 为 true,那么返回指定区域中非全局空间。
+     * 在没有指定 region 参数时(包括指定为空""),
+     * 如果指定 global 为 true,那么返回所有区域的全局空间。
+     * 如果没有指定 global 为 true,那么返回指定区域中所有的空间,包括全局空间。
+     * 在指定了line为 true 时,只返回 Line 空间;否则,只返回非 Line 空间。
+     * share 参数用于指定共享空间。
+     */
+
+    public function listbuckets(
+        $region = null,
+        $line = 'false',
+        $shared = 'false'
+    ) {
+        $path = '/v3/buckets?region=' . $region . '&line=' . $line . '&shared=' . $shared;
+        $info = $this->ucPost($path);
+        return $info;
+    }
+
+    /**
+     * 创建空间
+     *
+     * @param $name     创建的空间名
+     * @param $region    创建的区域,默认华东
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function createBucket($name, $region = 'z0')
+    {
+        $path = '/mkbucketv2/'.$name.'/region/' . $region;
+        return $this->rsPost($path, null);
+    }
+
+    /**
+     * 删除空间
+     *
+     * @param $name     删除的空间名
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function deleteBucket($name)
+    {
+        $path = '/drop/'.$name;
+        return $this->rsPost($path, null);
+    }
+
+    /**
+     * 获取指定空间绑定的所有的域名
+     *
+     * @return string[] 包含所有空间域名
+     */
+    public function domains($bucket)
+    {
+        return $this->apiGet('/v6/domain/list?tbl=' . $bucket);
+    }
+
+    /**
+     * 获取指定空间的相关信息
+     *
+     * @return string[] 包含空间信息
+     */
+    public function bucketInfo($bucket)
+    {
+        $path = '/v2/bucketInfo?bucket=' . $bucket;
+        $info = $this->ucPost($path);
+        return $info;
+    }
+
+    /**
+     * 获取指定zone的空间信息列表
+     * 在Region 未指定且Global 不为 true 时(包含未指定的情况,下同),返回用户的所有空间。
+     * 在指定了 region 参数且 global 不为 true 时,只列举非全局空间。
+     * shared 不指定shared参数或指定shared为rw或false时,返回包含具有读写权限空间,
+     * 指定shared为rd或true时,返回包含具有读权限空间。
+     * fs:如果为 true,会返回每个空间当前的文件数和存储量(实时数据)。
+     * @return string[] 包含空间信息
+     */
+    public function bucketInfos($region = null, $shared = 'false', $fs = 'false')
+    {
+        $path = '/v2/bucketInfos?region=' . $region . '&shared=' . $shared . '&fs=' . $fs;
+        $info = $this->ucPost($path);
+        return $info;
+    }
+
+    /**
+     * 获取空间绑定的域名列表
+     * @return string[] 包含空间绑定的所有域名
+     */
+
+    /**
+     * 列取空间的文件列表
+     *
+     * @param $bucket     空间名
+     * @param $prefix     列举前缀
+     * @param $marker     列举标识符
+     * @param $limit      单次列举个数限制
+     * @param $delimiter  指定目录分隔符
+     *
+     * @return array    包含文件信息的数组,类似:[
+*                                              {
+*                                                 "hash" => "<Hash string>",
+*                                                  "key" => "<Key string>",
+*                                                  "fsize" => "<file size>",
+*                                                  "putTime" => "<file modify time>"
+*                                              },
+*                                              ...
+*                                            ]
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/list.html
+     */
+    public function listFiles(
+        $bucket,
+        $prefix = null,
+        $marker = null,
+        $limit = 1000,
+        $delimiter = null
+    ) {
+        $query = array('bucket' => $bucket);
+        \Qiniu\setWithoutEmpty($query, 'prefix', $prefix);
+        \Qiniu\setWithoutEmpty($query, 'marker', $marker);
+        \Qiniu\setWithoutEmpty($query, 'limit', $limit);
+        \Qiniu\setWithoutEmpty($query, 'delimiter', $delimiter);
+        $url = $this->getRsfHost() . '/list?' . http_build_query($query);
+        return $this->get($url);
+    }
+
+    /**
+     * 列取空间的文件列表
+     *
+     * @param $bucket     空间名
+     * @param $prefix     列举前缀
+     * @param $marker     列举标识符
+     * @param $limit      单次列举个数限制
+     * @param $delimiter  指定目录分隔符
+     * @param $skipconfirm  是否跳过已删除条目的确认机制
+     *
+     * @return array    包含文件信息的数组,类似:[
+*                                              {
+*                                                 "hash" => "<Hash string>",
+*                                                  "key" => "<Key string>",
+*                                                  "fsize" => "<file size>",
+*                                                  "putTime" => "<file modify time>"
+*                                              },
+*                                              ...
+*                                            ]
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/list.html
+     */
+    public function listFilesv2(
+        $bucket,
+        $prefix = null,
+        $marker = null,
+        $limit = 1000,
+        $delimiter = null,
+        $skipconfirm = true
+    ) {
+        $query = array('bucket' => $bucket);
+        \Qiniu\setWithoutEmpty($query, 'prefix', $prefix);
+        \Qiniu\setWithoutEmpty($query, 'marker', $marker);
+        \Qiniu\setWithoutEmpty($query, 'limit', $limit);
+        \Qiniu\setWithoutEmpty($query, 'delimiter', $delimiter);
+        \Qiniu\setWithoutEmpty($query, 'skipconfirm', $skipconfirm);
+        $path = '/v2/list?' . http_build_query($query);
+        $url = $this->getRsfHost() . $path;
+        $headers = $this->auth->authorization($url, null, 'application/x-www-form-urlencoded');
+        $ret = Client::post($url, null, $headers);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = explode("\n", $ret->body);
+        $pop = array_pop($r);
+        return array($r, null);
+    }
+
+    /**
+     * 设置Referer防盗链
+     *
+     * @param $bucket     空间名
+     * @param $mode     0: 表示关闭Referer(使用此选项将会忽略以下参数并将恢复默认值);
+     * 1: 表示设置Referer白名单; 2:表示设置Referer黑名单
+     * @param $norefer     0: 表示不允许空 Refer 访问; 1: 表示允许空 Refer 访问
+     * @param $pattern      规则字符串, 当前允许格式分为三种: 一种为空主机头域名,
+     * 比如 foo.com; 一种是泛域名,比如 *.bar.com; 一种是完全通配符,
+     * 即一个 *; 多个规则之间用;隔开, 比如: foo.com;*.bar.com;sub.foo.com;*.sub.bar.com
+     * @param $source_enabled  源站是否支持,默认为0只给CDN配置, 设置为1表示开启源站防盗链
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    // public function referAntiLeech(){
+
+    // }
+
+    /**
+     * 增加bucket生命规则
+     *
+     * @param $bucket     空间名
+     * @param $name     规则名称 bucket 内唯一,长度小于50,不能为空,只能为
+     * 字母、数字、下划线
+     * @param $prefix     同一个 bucket 里面前缀不能重复
+     * @param $delete_after_days      指定上传文件多少天后删除,指定为0表示不删除,
+     * 大于0表示多少天后删除,需大于 to_line_after_days
+     * @param $to_line_after_days  指定文件上传多少天后转低频存储。指定为0表示
+     * 不转低频存储,小于0表示上传的文件立即变低频存储
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function bucketLifecycleRule(
+        $bucket,
+        $name,
+        $prefix,
+        $delete_after_days,
+        $to_line_after_days
+    ) {
+        $path = '/rules/add';
+        if ($bucket) {
+            $params['bucket'] = $bucket;
+        }
+        if ($name) {
+            $params['name'] = $name;
+        }
+        if ($prefix) {
+            $params['prefix'] = $prefix;
+        }
+        if ($delete_after_days) {
+            $params['delete_after_days'] = $delete_after_days;
+        }
+        if ($to_line_after_days) {
+            $params['to_line_after_days'] = $to_line_after_days;
+        }
+        $data = http_build_query($params);
+        $info = $this->ucPost($path, $data);
+        return $info;
+    }
+
+    /**
+     * 更新bucket生命规则
+     *
+     * @param $bucket     空间名
+     * @param $name     规则名称 bucket 内唯一,长度小于50,不能为空,只能为字母、
+     * 数字、下划线
+     * @param $prefix     同一个 bucket 里面前缀不能重复
+     * @param $delete_after_days      指定上传文件多少天后删除,指定为0表示不删除,
+     * 大于0表示多少天后删除,需大于 to_line_after_days
+     * @param $to_line_after_days  指定文件上传多少天后转低频存储。指定为0表示不
+     * 转低频存储,小于0表示上传的文件立即变低频存储
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function updateBucketLifecycleRule(
+        $bucket,
+        $name,
+        $prefix,
+        $delete_after_days,
+        $to_line_after_days
+    ) {
+        $path = '/rules/update';
+        if ($bucket) {
+            $params['bucket'] = $bucket;
+        }
+        if ($name) {
+            $params['name'] = $name;
+        }
+        if ($prefix) {
+            $params['prefix'] = $prefix;
+        }
+        if ($delete_after_days) {
+            $params['delete_after_days'] = $delete_after_days;
+        }
+        if ($to_line_after_days) {
+            $params['to_line_after_days'] = $to_line_after_days;
+        }
+        $data = http_build_query($params);
+        $info = $this->ucPost($path, $data);
+        return $info;
+    }
+
+    /**
+     * 获取bucket生命规则
+     *
+     * @param $bucket     空间名
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function getBucketLifecycleRules($bucket)
+    {
+        $path = '/rules/get?bucket=' . $bucket;
+        $info = $this->ucGet($path);
+        return $info;
+    }
+
+    /**
+     * 删除bucket生命规则
+     *
+     * @param $bucket     空间名
+     * @param $name     规则名称 bucket 内唯一,长度小于50,不能为空,
+     * 只能为字母、数字、下划线()
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function deleteBucketLifecycleRule($bucket, $name)
+    {
+        $path = '/rules/delete';
+        if ($bucket) {
+            $params['bucket'] = $bucket;
+        }
+        if ($name) {
+            $params['name'] = $name;
+        }
+        $data = http_build_query($params);
+        $info = $this->ucPost($path, $data);
+        return $info;
+    }
+
+    /**
+     * 增加bucket事件通知规则
+     *
+     * @param $bucket     空间名
+     * @param $name     规则名称 bucket 内唯一,长度小于50,不能为空,
+     * 只能为字母、数字、下划线()
+     * @param $prefix     同一个 bucket 里面前缀不能重复
+     * @param $suffix      可选,文件配置的后缀
+     * @param $event  事件类型,可以指定多个,包括 put,mkfile,delete,copy,move,append,
+     * disable,enable,deleteMarkerCreate
+     * @param $callbackURL 通知URL,可以指定多个,失败依次重试
+     * @param $access_key 可选,设置的话会对通知请求用对应的ak、sk进行签名
+     * @param $host 可选,通知请求的host
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function putBucketEvent(
+        $bucket,
+        $name,
+        $prefix,
+        $suffix,
+        $event,
+        $callbackURL,
+        $access_key = null,
+        $host = null
+    ) {
+        $path = '/events/add';
+        if ($bucket) {
+            $params['bucket'] = $bucket;
+        }
+        if ($name) {
+            $params['name'] = $name;
+        }
+        if ($prefix) {
+            $params['prefix'] = $prefix;
+        }
+        if ($suffix) {
+            $params['suffix'] = $suffix;
+        }
+        if ($callbackURL) {
+            $params['callbackURL'] = $callbackURL;
+        }
+        if ($access_key) {
+            $params['access_key'] = $access_key;
+        }
+        if ($host) {
+            $params['host'] = $host;
+        }
+        $data = http_build_query($params);
+        if ($event) {
+            $eventpath = "";
+            foreach ($event as $key => $value) {
+                $eventpath .= "&event=$value";
+            }
+            $data .= $eventpath;
+        }
+        $info = $this->ucPost($path, $data);
+        return $info;
+    }
+
+    /**
+     * 更新bucket事件通知规则
+     *
+     * @param $bucket     空间名
+     * @param $name     规则名称 bucket 内唯一,长度小于50,不能为空,
+     * 只能为字母、数字、下划线()
+     * @param $prefix     同一个 bucket 里面前缀不能重复
+     * @param $suffix      可选,文件配置的后缀
+     * @param $event  事件类型,可以指定多个,包括 put,mkfile,delete,copy,move,append,disable,
+     * enable,deleteMarkerCreate
+     * @param $callbackURL 通知URL,可以指定多个,失败依次重试
+     * @param $access_key 可选,设置的话会对通知请求用对应的ak、sk进行签名
+     * @param $host 可选,通知请求的host
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function updateBucketEvent(
+        $bucket,
+        $name,
+        $prefix,
+        $suffix,
+        $event,
+        $callbackURL,
+        $access_key = null,
+        $host = null
+    ) {
+        $path = '/events/update';
+        if ($bucket) {
+            $params['bucket'] = $bucket;
+        }
+        if ($name) {
+            $params['name'] = $name;
+        }
+        if ($prefix) {
+            $params['prefix'] = $prefix;
+        }
+        if ($suffix) {
+            $params['suffix'] = $suffix;
+        }
+        if ($event) {
+            $params['event'] = $event;
+        }
+        if ($callbackURL) {
+            $params['callbackURL'] = $callbackURL;
+        }
+        if ($access_key) {
+            $params['access_key'] = $access_key;
+        }
+        if ($host) {
+            $params['host'] = $host;
+        }
+        $data = http_build_query($params);
+        $info = $this->ucPost($path, $data);
+        return $info;
+    }
+
+    /**
+     * 获取bucket事件通知规则
+     *
+     * @param $bucket     空间名
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function getBucketEvents($bucket)
+    {
+        $path = '/events/get?bucket=' . $bucket;
+        $info = $this->ucGet($path);
+        return $info;
+    }
+
+    /**
+     * 删除bucket事件通知规则
+     *
+     * @param $bucket     空间名
+     * @param $name     规则名称 bucket 内唯一,长度小于50,不能为空,
+     * 只能为字母、数字、下划线
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function deleteBucketEvent($bucket, $name)
+    {
+        $path = '/events/delete';
+        if ($bucket) {
+            $params['bucket'] = $bucket;
+        }
+        if ($name) {
+            $params['name'] = $name;
+        }
+        $data = http_build_query($params);
+        $info = $this->ucPost($path, $data);
+        return $info;
+    }
+
+    /**
+     * 设置bucket的跨域信息,最多允许设置10条跨域规则。
+     * 对于同一个域名如果设置了多条规则,那么按顺序使用第一条匹配的规则去生成返回值。
+     * 对于简单跨域请求,只匹配 Origin;
+     * allowed_orgin: 允许的域名。必填;支持通配符*;*表示全部匹配;只有第一个*生效;
+     * 需要设置"Scheme";大小写敏感。例如
+     * 规则:http://*.abc.*.com 请求:"http://test.abc.test.com" 结果:不通过
+     * 规则:"http://abc.com" 请求:"https://abc.com"/"abc.com" 结果:不通过
+     * 规则:"abc.com" 请求:"http://abc.com" 结果:不通过
+     * allowed_method: 允许的方法。必填;不支持通配符;大小写不敏感;
+     * allowed_header: 允许的header。选填;支持通配符*,
+     * 但只能是单独的*,表示允许全部header,其他*不生效;
+     * 空则不允许任何header;大小写不敏感;
+     * exposed_header: 暴露的header。选填;不支持通配符;
+     * X-Log, X-Reqid是默认会暴露的两个header;
+     * 其他的header如果没有设置,则不会暴露;大小写不敏感;
+     * max_age: 结果可以缓存的时间。选填;空则不缓存;
+     * allowed_credentials:该配置不支持设置,默认为true。
+     * 备注:如果没有设置任何corsRules,那么默认允许所有的跨域请求
+     */
+    // public function putCorsRules(string $bucket, array $params)
+    // {
+    //     $path = '/corsRules/set/' . $bucket;
+    //     $data = json_encode($params);
+    //     $info = $this->ucPost($path, $data);
+    //     return $info;
+    // }
+
+    /**
+     * 获取bucket的跨域信息
+     * $bucket 空间名
+     */
+    public function getCorsRules($bucket)
+    {
+        $path = '/corsRules/get/' . $bucket;
+        $info = $this->ucGet($path);
+        return $info;
+    }
+
+    /**
+     * 设置回源规则
+     * 使用该API设置源站优先级高于/image设置的源站,即IO优先读取source接口设置的源站配置,
+     * 如果存在会忽略/image设置的源站
+     * Bucket 空间名
+     * Host(可选)回源Host
+     * RetryCodes(可选),镜像回源时源站返回Code可以重试,最多指定3个,当前只支持4xx错误码重试
+     * SourceQiniuAK,SourceQiniuSK(可选)如果存在将在回源时对URL进行签名,客户源站可以验证
+     * 以保证请求来自Qiniu服务器
+     * Expires(可选) 签名过期时间,如果不设置默认为1小时
+     * Addr 回源地址,不可重复。
+     * Weight 权重,范围限制1-100,不填默认为1,回源时会根据所有源的权重值进行源站选择,
+     * 主备源会分开计算.
+     * Backup 是否备用回源,回源优先尝试主源
+     */
+    // public function putBucktSourceConfig(array $params)
+    // {
+    //     $path = '/mirrorConfig/set';
+    //     $data = json_encode($params);
+    //     $info = $this->ucPostV2($path, $data);
+    //     return $info;
+    // }
+
+    /**
+     * 获取空间回源配置
+     */
+    public function getBucktSourceConfig(array $params)
+    {
+        $path = '/mirrorConfig/get';
+        $data = json_encode($params);
+        $info = $this->ucPostV2($path, $data);
+        return $info;
+    }
+
+    /**
+     * 开关原图保护
+     * mode 为1表示开启原图保护,0表示关闭
+     */
+    public function putBucketAccessStyleMode($bucket, $mode)
+    {
+        $path = '/accessMode/' . $bucket . '/mode/' . $mode;
+        $info = $this->ucPost($path, null);
+        return $info;
+    }
+
+    /**
+     * 设置私有属性
+     * private为0表示公开,为1表示私有
+     */
+    public function putBucketAccessMode($bucket, $private)
+    {
+        $path = '/bucket/' . $bucket . '/private/' . $private;
+        $info = $this->ucPost($path, null);
+        return $info;
+    }
+
+    /**
+     * 设置referer防盗链
+     * bucket=<BucketName>: bucket 名
+     * mode=<AntiLeechMode>:
+     * 0: 表示关闭Referer(使用此选项将会忽略以下参数并将恢复默认值);
+     * 1: 表示设置Referer白名单; 2: 表示设置Referer黑名单
+     * norefer=<NoRefer>: 0: 表示不允许空 Refer 访问;
+     * 1: 表示允许空 Refer 访问
+     * pattern=<Pattern>: 规则字符串, 当前允许格式分为三种:
+     * 一种为空主机头域名, 比如 foo.com;
+     * 一种是泛域名, 比如 *.bar.com; 一种是完全通配符, 即一个 *;
+     * 多个规则之间用;隔开, 比如: foo.com;*.bar.com;sub.foo.com;*.sub.bar.com
+     * 空主机头域名可以是多级域名,比如 foo.bar.com。
+     * 多个域名之间不允许夹带空白字符。
+     * source_enabled=:1
+     */
+    public function putReferAntiLeech($bucket, $mode, $norefer, $pattern, $enabled = 1)
+    {
+        $path = "/referAntiLeech?bucket=$bucket&mode=$mode&norefer=$norefer&pattern=$pattern&source_enabled=$enabled";
+        $info = $this->ucPost($path, null);
+        return $info;
+    }
+
+    /**
+     * 设置Bucket的maxAge
+     * maxAge为0或者负数表示为默认值(31536000)
+     */
+    public function putBucketMaxAge($bucket, $maxAge)
+    {
+        $path = '/maxAge?bucket=' . $bucket . '&maxAge=' . $maxAge;
+        $info = $this->ucPost($path, null);
+        return $info;
+    }
+
+    /**
+     * 设置配额
+     * <bucket>: 空间名称,不支持授权空间
+     * <size>: 空间存储量配额,参数传入0或不传表示不更改当前配置,传入-1表示取消限额,
+     * 新创建的空间默认没有限额。
+     * <count>: 空间文件数配额,参数含义同<size>
+     */
+    public function putBucketQuota($bucket, $size, $count)
+    {
+        $path = '/setbucketquota/' . $bucket . '/size/' . $size . '/count/' . $count;
+        $info = $this->apiPost($path, null);
+        return $info;
+    }
+
+    /**
+     * 获取配额
+     * bucket 空间名称
+     */
+    public function getBucketQuota($bucket)
+    {
+        $path = '/getbucketquota/' . $bucket;
+        $info = $this->apiPost($path, null);
+        return $info;
+    }
+
+    /**
+     * 获取资源的元信息,但不返回文件内容
+     *
+     * @param $bucket     待获取信息资源所在的空间
+     * @param $key        待获取资源的文件名
+     *
+     * @return array    包含文件信息的数组,类似:
+*                                              [
+*                                                  "hash" => "<Hash string>",
+*                                                  "key" => "<Key string>",
+*                                                  "fsize" => <file size>,
+*                                                  "putTime" => "<file modify time>"
+*                                                  "fileType" => <file type>
+*                                              ]
+     *
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/stat.html
+     */
+    public function stat($bucket, $key)
+    {
+        $path = '/stat/' . \Qiniu\entry($bucket, $key);
+        return $this->rsGet($path);
+    }
+
+    /**
+     * 删除指定资源
+     *
+     * @param $bucket     待删除资源所在的空间
+     * @param $key        待删除资源的文件名
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/delete.html
+     */
+    public function delete($bucket, $key)
+    {
+        $path = '/delete/' . \Qiniu\entry($bucket, $key);
+        list(, $error) = $this->rsPost($path);
+        return $error;
+    }
+
+
+    /**
+     * 给资源进行重命名,本质为move操作。
+     *
+     * @param $bucket     待操作资源所在空间
+     * @param $oldname    待操作资源文件名
+     * @param $newname    目标资源文件名
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     */
+    public function rename($bucket, $oldname, $newname)
+    {
+        return $this->move($bucket, $oldname, $bucket, $newname);
+    }
+
+    /**
+     * 对资源进行复制。
+     *
+     * @param $from_bucket     待操作资源所在空间
+     * @param $from_key        待操作资源文件名
+     * @param $to_bucket       目标资源空间名
+     * @param $to_key          目标资源文件名
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/copy.html
+     */
+    public function copy($from_bucket, $from_key, $to_bucket, $to_key, $force = false)
+    {
+        $from = \Qiniu\entry($from_bucket, $from_key);
+        $to = \Qiniu\entry($to_bucket, $to_key);
+        $path = '/copy/' . $from . '/' . $to;
+        if ($force === true) {
+            $path .= '/force/true';
+        }
+        list(, $error) = $this->rsPost($path);
+        return $error;
+    }
+
+    /**
+     * 将资源从一个空间到另一个空间
+     *
+     * @param $from_bucket     待操作资源所在空间
+     * @param $from_key        待操作资源文件名
+     * @param $to_bucket       目标资源空间名
+     * @param $to_key          目标资源文件名
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/move.html
+     */
+    public function move($from_bucket, $from_key, $to_bucket, $to_key, $force = false)
+    {
+        $from = \Qiniu\entry($from_bucket, $from_key);
+        $to = \Qiniu\entry($to_bucket, $to_key);
+        $path = '/move/' . $from . '/' . $to;
+        if ($force) {
+            $path .= '/force/true';
+        }
+        list(, $error) = $this->rsPost($path);
+        return $error;
+    }
+
+    /**
+     * 主动修改指定资源的文件元信息
+     *
+     * @param $bucket     待操作资源所在空间
+     * @param $key        待操作资源文件名
+     * @param $mime       待操作文件目标mimeType
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/chgm.html
+     */
+    public function changeMime($bucket, $key, $mime)
+    {
+        $resource = \Qiniu\entry($bucket, $key);
+        $encode_mime = \Qiniu\base64_urlSafeEncode($mime);
+        $path = '/chgm/' . $resource . '/mime/' . $encode_mime;
+        list(, $error) = $this->rsPost($path);
+        return $error;
+    }
+
+
+    /**
+     * 修改指定资源的存储类型
+     *
+     * @param $bucket     待操作资源所在空间
+     * @param $key        待操作资源文件名
+     * @param $fileType       待操作文件目标文件类型
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  https://developer.qiniu.com/kodo/api/3710/modify-the-file-type
+     */
+    public function changeType($bucket, $key, $fileType)
+    {
+        $resource = \Qiniu\entry($bucket, $key);
+        $path = '/chtype/' . $resource . '/type/' . $fileType;
+        list(, $error) = $this->rsPost($path);
+        return $error;
+    }
+
+    /**
+     * 修改文件的存储状态,即禁用状态和启用状态间的的互相转换
+     *
+     * @param $bucket     待操作资源所在空间
+     * @param $key        待操作资源文件名
+     * @param $status       待操作文件目标文件类型
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  https://developer.qiniu.com/kodo/api/4173/modify-the-file-status
+     */
+    public function changeStatus($bucket, $key, $status)
+    {
+        $resource = \Qiniu\entry($bucket, $key);
+        $path = '/chstatus/' . $resource . '/status/' . $status;
+        list(, $error) = $this->rsPost($path);
+        return $error;
+    }
+
+    /**
+     * 从指定URL抓取资源,并将该资源存储到指定空间中
+     *
+     * @param $url        指定的URL
+     * @param $bucket     目标资源空间
+     * @param $key        目标资源文件名
+     *
+     * @return array    包含已拉取的文件信息。
+     *                         成功时:  [
+     *                                          [
+     *                                              "hash" => "<Hash string>",
+     *                                              "key" => "<Key string>"
+     *                                          ],
+     *                                          null
+     *                                  ]
+     *
+     *                         失败时:  [
+     *                                          null,
+     *                                         Qiniu/Http/Error
+     *                                  ]
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/fetch.html
+     */
+    public function fetch($url, $bucket, $key = null)
+    {
+
+        $resource = \Qiniu\base64_urlSafeEncode($url);
+        $to = \Qiniu\entry($bucket, $key);
+        $path = '/fetch/' . $resource . '/to/' . $to;
+
+        $ak = $this->auth->getAccessKey();
+        $ioHost = $this->config->getIovipHost($ak, $bucket);
+
+        $url = $ioHost . $path;
+        return $this->post($url, null);
+    }
+
+    /**
+     * 从镜像源站抓取资源到空间中,如果空间中已经存在,则覆盖该资源
+     *
+     * @param $bucket     待获取资源所在的空间
+     * @param $key        代获取资源文件名
+     *
+     * @return mixed      成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  http://developer.qiniu.com/docs/v6/api/reference/rs/prefetch.html
+     */
+    public function prefetch($bucket, $key)
+    {
+        $resource = \Qiniu\entry($bucket, $key);
+        $path = '/prefetch/' . $resource;
+
+        $ak = $this->auth->getAccessKey();
+        $ioHost = $this->config->getIovipHost($ak, $bucket);
+
+        $url = $ioHost . $path;
+        list(, $error) = $this->post($url, null);
+        return $error;
+    }
+
+    /**
+     * 在单次请求中进行多个资源管理操作
+     *
+     * @param $operations     资源管理操作数组
+     *
+     * @return array 每个资源的处理情况,结果类似:
+     *              [
+     *                   { "code" => <HttpCode int>, "data" => <Data> },
+     *                   { "code" => <HttpCode int> },
+     *                   { "code" => <HttpCode int> },
+     *                   { "code" => <HttpCode int> },
+     *                   { "code" => <HttpCode int>, "data" => { "error": "<ErrorMessage string>" } },
+     *                   ...
+     *               ]
+     * @link http://developer.qiniu.com/docs/v6/api/reference/rs/batch.html
+     */
+    public function batch($operations)
+    {
+        $params = 'op=' . implode('&op=', $operations);
+        return $this->rsPost('/batch', $params);
+    }
+
+    /**
+     * 设置文件的生命周期
+     *
+     * @param $bucket 设置文件生命周期文件所在的空间
+     * @param $key    设置文件生命周期文件的文件名
+     * @param $days   设置该文件多少天后删除,当$days设置为0时表示取消该文件的生命周期
+     *
+     * @return Mixed
+     * @link https://developer.qiniu.com/kodo/api/update-file-lifecycle
+     */
+    public function deleteAfterDays($bucket, $key, $days)
+    {
+        $entry = \Qiniu\entry($bucket, $key);
+        $path = "/deleteAfterDays/$entry/$days";
+        list(, $error) = $this->rsPost($path);
+        return $error;
+    }
+
+    private function getRsfHost()
+    {
+        $scheme = "http://";
+        if ($this->config->useHTTPS == true) {
+            $scheme = "https://";
+        }
+        return $scheme . Config::RSF_HOST;
+    }
+
+    private function getRsHost()
+    {
+        $scheme = "http://";
+        if ($this->config->useHTTPS == true) {
+            $scheme = "https://";
+        }
+        return $scheme . Config::RS_HOST;
+    }
+
+    private function getApiHost()
+    {
+        $scheme = "http://";
+        if ($this->config->useHTTPS == true) {
+            $scheme = "https://";
+        }
+        return $scheme . Config::API_HOST;
+    }
+
+    private function getUcHost()
+    {
+        $scheme = "http://";
+        if ($this->config->useHTTPS == true) {
+            $scheme = "https://";
+        }
+        return $scheme . Config::UC_HOST;
+    }
+
+    private function rsPost($path, $body = null)
+    {
+        $url = $this->getRsHost() . $path;
+        return $this->post($url, $body);
+    }
+
+    private function apiPost($path, $body = null)
+    {
+        $url = $this->getApiHost() . $path;
+        return $this->post($url, $body);
+    }
+
+    private function ucPost($path, $body = null)
+    {
+        $url = $this->getUcHost() . $path;
+        return $this->post($url, $body);
+    }
+
+    private function ucGet($path)
+    {
+        $url = $this->getUcHost() . $path;
+        return $this->get($url);
+    }
+
+    private function apiGet($path)
+    {
+        $url = $this->getApiHost() . $path;
+        return $this->get($url);
+    }
+
+    private function rsGet($path)
+    {
+        $url = $this->getRsHost() . $path;
+        return $this->get($url);
+    }
+
+    private function get($url)
+    {
+        $headers = $this->auth->authorization($url);
+        $ret = Client::get($url, $headers);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        return array($ret->json(), null);
+    }
+
+    private function post($url, $body)
+    {
+        $headers = $this->auth->authorization($url, $body, 'application/x-www-form-urlencoded');
+        $ret = Client::post($url, $body, $headers);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+
+    private function ucPostV2($path, $body)
+    {
+        $url = $this->getUcHost() . $path;
+        return $this->postV2($url, $body);
+    }
+
+    private function postV2($url, $body)
+    {
+        $headers = $this->auth->authorizationV2($url, 'POST', $body, 'application/json');
+        $headers["Content-Type"] = 'application/json';
+        $ret = Client::post($url, $body, $headers);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+
+    public static function buildBatchCopy($source_bucket, $key_pairs, $target_bucket, $force)
+    {
+        return self::twoKeyBatch('/copy', $source_bucket, $key_pairs, $target_bucket, $force);
+    }
+
+
+    public static function buildBatchRename($bucket, $key_pairs, $force)
+    {
+        return self::buildBatchMove($bucket, $key_pairs, $bucket, $force);
+    }
+
+
+    public static function buildBatchMove($source_bucket, $key_pairs, $target_bucket, $force)
+    {
+        return self::twoKeyBatch('/move', $source_bucket, $key_pairs, $target_bucket, $force);
+    }
+
+
+    public static function buildBatchDelete($bucket, $keys)
+    {
+        return self::oneKeyBatch('/delete', $bucket, $keys);
+    }
+
+
+    public static function buildBatchStat($bucket, $keys)
+    {
+        return self::oneKeyBatch('/stat', $bucket, $keys);
+    }
+
+    public static function buildBatchDeleteAfterDays($bucket, $key_day_pairs)
+    {
+        $data = array();
+        foreach ($key_day_pairs as $key => $day) {
+            array_push($data, '/deleteAfterDays/' . \Qiniu\entry($bucket, $key) . '/' . $day);
+        }
+        return $data;
+    }
+
+    public static function buildBatchChangeMime($bucket, $key_mime_pairs)
+    {
+        $data = array();
+        foreach ($key_mime_pairs as $key => $mime) {
+            array_push($data, '/chgm/' . \Qiniu\entry($bucket, $key) . '/mime/' . base64_encode($mime));
+        }
+        return $data;
+    }
+
+    public static function buildBatchChangeType($bucket, $key_type_pairs)
+    {
+        $data = array();
+        foreach ($key_type_pairs as $key => $type) {
+            array_push($data, '/chtype/' . \Qiniu\entry($bucket, $key) . '/type/' . $type);
+        }
+        return $data;
+    }
+
+    private static function oneKeyBatch($operation, $bucket, $keys)
+    {
+        $data = array();
+        foreach ($keys as $key) {
+            array_push($data, $operation . '/' . \Qiniu\entry($bucket, $key));
+        }
+        return $data;
+    }
+
+    private static function twoKeyBatch($operation, $source_bucket, $key_pairs, $target_bucket, $force)
+    {
+        if ($target_bucket === null) {
+            $target_bucket = $source_bucket;
+        }
+        $data = array();
+        $forceOp = "false";
+        if ($force) {
+            $forceOp = "true";
+        }
+        foreach ($key_pairs as $from_key => $to_key) {
+            $from = \Qiniu\entry($source_bucket, $from_key);
+            $to = \Qiniu\entry($target_bucket, $to_key);
+            array_push($data, $operation . '/' . $from . '/' . $to . "/force/" . $forceOp);
+        }
+        return $data;
+    }
+}

+ 122 - 0
addons/qiniu/library/Qiniu/Storage/FormUploader.php

@@ -0,0 +1,122 @@
+<?php
+
+namespace Qiniu\Storage;
+
+use Qiniu\Config;
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+
+final class FormUploader
+{
+
+    /**
+     * 上传二进制流到七牛, 内部使用
+     *
+     * @param string   $upToken 上传凭证
+     * @param string   $key     上传文件名
+     * @param resource $data    上传二进制流
+     * @param Config   $config  上传配置
+     * @param array    $params  自定义变量,规格参考 http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
+     * @param string   $mime    上传数据的mimeType
+     *
+     * @return array    包含已上传文件的信息,类似:
+     *                                                  [
+     *                                                  "hash" => "<Hash string>",
+     *                                                  "key" => "<Key string>"
+     *                                                  ]
+     */
+    public static function put($upToken, $key, $data, $config, $params, $mime, $fname)
+    {
+        $fields = array('token' => $upToken);
+        if ($key === null) {
+        } else {
+            $fields['key'] = $key;
+        }
+
+        //enable crc32 check by default
+        $fields['crc32'] = \Qiniu\crc32_data($data);
+
+        if ($params) {
+            foreach ($params as $k => $v) {
+                $fields[$k] = $v;
+            }
+        }
+
+        list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($upToken);
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        $upHost = $config->getUpHost($accessKey, $bucket);
+
+        $response = Client::multipartPost($upHost, $fields, 'file', $fname, $data, $mime);
+        if (!$response->ok()) {
+            return array(null, new Error($upHost, $response));
+        }
+        return array($response->json(), null);
+    }
+
+    /**
+     * 上传文件到七牛,内部使用
+     *
+     * @param string $upToken  上传凭证
+     * @param string $key      上传文件名
+     * @param string $filePath 上传文件的路径
+     * @param Config $config   上传配置
+     * @param array  $params   自定义变量,规格参考 http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
+     * @param string $mime     上传数据的mimeType
+     *
+     * @return array    包含已上传文件的信息,类似:
+     *                                                  [
+     *                                                  "hash" => "<Hash string>",
+     *                                                  "key" => "<Key string>"
+     *                                                  ]
+     */
+    public static function putFile($upToken, $key, $filePath, $config, $params, $mime)
+    {
+        $fields = array('token' => $upToken, 'file' => self::createFile($filePath, $mime));
+        if ($key !== null) {
+            $fields['key'] = $key;
+        }
+
+        $fields['crc32'] = \Qiniu\crc32_file($filePath);
+
+        if ($params) {
+            foreach ($params as $k => $v) {
+                $fields[$k] = $v;
+            }
+        }
+        $fields['key'] = $key;
+        $headers = array('Content-Type' => 'multipart/form-data');
+
+        list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($upToken);
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        $upHost = $config->getUpHost($accessKey, $bucket);
+
+        $response = Client::post($upHost, $fields, $headers);
+        if (!$response->ok()) {
+            return array(null, new Error($upHost, $response));
+        }
+        return array($response->json(), null);
+    }
+
+    private static function createFile($filename, $mime)
+    {
+        // PHP 5.5 introduced a CurlFile object that deprecates the old @filename syntax
+        // See: https://wiki.php.net/rfc/curl-file-upload
+        if (function_exists('curl_file_create')) {
+            return curl_file_create($filename, $mime);
+        }
+
+        // Use the old style if using an older version of PHP
+        $value = "@{$filename}";
+        if (!empty($mime)) {
+            $value .= ';type=' . $mime;
+        }
+
+        return $value;
+    }
+}

+ 210 - 0
addons/qiniu/library/Qiniu/Storage/ResumeUploader.php

@@ -0,0 +1,210 @@
+<?php
+
+namespace Qiniu\Storage;
+
+use Qiniu\Config;
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+
+/**
+ * 断点续上传类, 该类主要实现了断点续上传中的分块上传,
+ * 以及相应地创建块和创建文件过程.
+ *
+ * @link http://developer.qiniu.com/docs/v6/api/reference/up/mkblk.html
+ * @link http://developer.qiniu.com/docs/v6/api/reference/up/mkfile.html
+ */
+final class ResumeUploader
+{
+    private $upToken;
+    private $key;
+    private $inputStream;
+    private $size;
+    private $params;
+    private $mime;
+    private $contexts;
+    private $host;
+    private $currentUrl;
+    private $config;
+
+    /**
+     * 上传二进制流到七牛
+     *
+     * @param string   $upToken     上传凭证
+     * @param string   $key         上传文件名
+     * @param resource $inputStream 上传二进制流
+     * @param int      $size        上传流的大小
+     * @param array    $params      自定义变量
+     * @param string   $mime        上传数据的mimeType
+     *
+     * @link http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
+     */
+    public function __construct(
+        $upToken,
+        $key,
+        $inputStream,
+        $size,
+        $params = null,
+        $mime = '',
+        $config = null
+    ) {
+
+        $this->upToken = $upToken;
+        $this->key = $key;
+        $this->inputStream = $inputStream;
+        $this->size = $size;
+        $this->params = $params;
+        $this->mime = $mime ? $mime : 'application/octet-stream';
+        $this->contexts = array();
+        $this->config = $config ? $config : new Config();
+
+        list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($upToken);
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        $upHost = $this->config->getUpHost($accessKey, $bucket);
+        if ($err != null) {
+            throw new \Exception($err->message(), 1);
+        }
+        $this->host = $upHost;
+    }
+
+    /**
+     * 上传操作
+     */
+    public function upload($fname)
+    {
+        $uploaded = 0;
+        while ($uploaded < $this->size) {
+            $blockSize = $this->blockSize($uploaded);
+            $data = fread($this->inputStream, $blockSize);
+            if ($data === false) {
+                throw new \Exception("file read failed", 1);
+            }
+            $crc = \Qiniu\crc32_data($data);
+            $response = $this->makeBlock($data, $blockSize);
+            $ret = null;
+            if ($response->ok() && $response->json() != null) {
+                $ret = $response->json();
+            }
+            if ($response->statusCode < 0) {
+                list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($this->upToken);
+                if ($err != null) {
+                    return array(null, $err);
+                }
+
+                $upHostBackup = $this->config->getUpBackupHost($accessKey, $bucket);
+                $this->host = $upHostBackup;
+            }
+            if ($response->needRetry() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
+                $response = $this->makeBlock($data, $blockSize);
+                $ret = $response->json();
+            }
+
+            if (!$response->ok() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
+                return array(null, new Error($this->currentUrl, $response));
+            }
+            array_push($this->contexts, $ret['ctx']);
+            $uploaded += $blockSize;
+        }
+        return $this->makeFile($fname);
+    }
+
+    public function uploadChunk($index, $file, $size)
+    {
+        $blockSize = $this->size;
+        $data = fread($this->inputStream, $size);
+        if ($data === false) {
+            throw new \Exception("file read failed", 1);
+        }
+        $crc = \Qiniu\crc32_data($data);
+        $response = $this->makeBlock($data, $blockSize);
+        $ret = null;
+        if ($response->ok() && $response->json() != null) {
+            $ret = $response->json();
+        }
+        if ($response->statusCode < 0) {
+            list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($this->upToken);
+            if ($err != null) {
+                return array(null, $err);
+            }
+
+            $upHostBackup = $this->config->getUpBackupHost($accessKey, $bucket);
+            $this->host = $upHostBackup;
+        }
+        if ($response->needRetry() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
+            $response = $this->makeBlock($data, $blockSize);
+            $ret = $response->json();
+        }
+
+        if (!$response->ok() || !isset($ret['crc32']) || $crc != $ret['crc32']) {
+            return array(null, new Error($this->currentUrl, $response));
+        }
+        array_push($this->contexts, $ret['ctx']);
+        return $ret;
+    }
+
+    public function setContexts($contexts)
+    {
+        $this->contexts = is_array($contexts) ? $contexts : explode(',', $contexts);
+        return $this;
+    }
+
+    /**
+     * 创建块
+     */
+    private function makeBlock($block, $blockSize)
+    {
+        $url = $this->host . '/mkblk/' . $blockSize;
+        return $this->post($url, $block);
+    }
+
+    private function fileUrl($fname)
+    {
+        $url = $this->host . '/mkfile/' . $this->size;
+        $url .= '/mimeType/' . \Qiniu\base64_urlSafeEncode($this->mime);
+        if ($this->key != null) {
+            $url .= '/key/' . \Qiniu\base64_urlSafeEncode($this->key);
+        }
+        $url .= '/fname/' . \Qiniu\base64_urlSafeEncode($fname);
+        if (!empty($this->params)) {
+            foreach ($this->params as $key => $value) {
+                $val = \Qiniu\base64_urlSafeEncode($value);
+                $url .= "/$key/$val";
+            }
+        }
+        return $url;
+    }
+
+    /**
+     * 创建文件
+     */
+    public function makeFile($fname)
+    {
+        $url = $this->fileUrl($fname);
+        $body = implode(',', $this->contexts);
+        $response = $this->post($url, $body);
+        if ($response->needRetry()) {
+            $response = $this->post($url, $body);
+        }
+        if (!$response->ok()) {
+            return array(null, new Error($this->currentUrl, $response));
+        }
+        return array($response->json(), null);
+    }
+
+    private function post($url, $data)
+    {
+        $this->currentUrl = $url;
+        $headers = array('Authorization' => 'UpToken ' . $this->upToken);
+        return Client::post($url, $data, $headers);
+    }
+
+    private function blockSize($uploaded)
+    {
+        if ($this->size < $uploaded + Config::BLOCK_SIZE) {
+            return $this->size - $uploaded;
+        }
+        return Config::BLOCK_SIZE;
+    }
+}

+ 106 - 0
addons/qiniu/library/Qiniu/Storage/UploadManager.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace Qiniu\Storage;
+
+use Qiniu\Config;
+use Qiniu\Http\HttpClient;
+use Qiniu\Storage\ResumeUploader;
+use Qiniu\Storage\FormUploader;
+
+/**
+ * 主要涉及了资源上传接口的实现
+ *
+ * @link http://developer.qiniu.com/docs/v6/api/reference/up/
+ */
+final class UploadManager
+{
+    private $config;
+
+    public function __construct(Config $config = null)
+    {
+        if ($config === null) {
+            $config = new Config();
+        }
+        $this->config = $config;
+    }
+
+    /**
+     * 上传二进制流到七牛
+     *
+     * @param string   $upToken  上传凭证
+     * @param string   $key      上传文件名
+     * @param resource $data     上传二进制流
+     * @param array    $params   自定义变量,规格参考 http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
+     * @param string   $mime     上传数据的mimeType
+     * @param bool     $checkCrc 是否校验crc32
+     *
+     * @return array    包含已上传文件的信息,类似:
+     *                                              [
+     *                                                  "hash" => "<Hash string>",
+     *                                                  "key" => "<Key string>"
+     *                                              ]
+     */
+    public function put($upToken, $key, $data, $params = null, $mime = 'application/octet-stream', $fname = "default_filename")
+    {
+        $params = self::trimParams($params);
+        return FormUploader::put($upToken, $key, $data, $this->config, $params, $mime, $fname);
+    }
+
+
+    /**
+     * 上传文件到七牛
+     *
+     * @param string $upToken  上传凭证
+     * @param string $key      上传文件名
+     * @param string $filePath 上传文件的路径
+     * @param array  $params   自定义变量,规格参考 http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
+     * @param string $mime     上传数据的mimeType
+     * @param bool   $checkCrc 是否校验crc32
+     *
+     * @return array    包含已上传文件的信息,类似:
+     *                                              [
+     *                                                  "hash" => "<Hash string>",
+     *                                                  "key" => "<Key string>"
+     *                                              ]
+     */
+    public function putFile($upToken, $key, $filePath, $params = null, $mime = 'application/octet-stream', $checkCrc = false)
+    {
+        $file = fopen($filePath, 'rb');
+        if ($file === false) {
+            throw new \Exception("file can not open", 1);
+        }
+        $params = self::trimParams($params);
+        $stat = fstat($file);
+        $size = $stat['size'];
+        //不满足分片上传条件
+        if ($size <= Config::BLOCK_SIZE) {
+            $data = fread($file, $size);
+            fclose($file);
+            if ($data === false) {
+                throw new \Exception("file can not read", 1);
+            }
+            return FormUploader::put($upToken, $key, $data, $this->config, $params, $mime, basename($filePath));
+        } else {
+            $up = new ResumeUploader($upToken, $key, $file, $size, $params, $mime, $this->config);
+            $ret = $up->upload(basename($filePath));
+            fclose($file);
+            return $ret;
+        }
+    }
+
+    public static function trimParams($params)
+    {
+        if ($params === null) {
+            return null;
+        }
+        $ret = array();
+        foreach ($params as $k => $v) {
+            $pos1 = strpos($k, 'x:');
+            $pos2 = strpos($k, 'x-qn-meta-');
+            if (($pos1 === 0 || $pos2 === 0) && !empty($v)) {
+                $ret[$k] = $v;
+            }
+        }
+        return $ret;
+    }
+}

+ 47 - 0
addons/qiniu/library/Qiniu/Zone.php

@@ -0,0 +1,47 @@
+<?php
+namespace Qiniu;
+
+use Qiniu\Region;
+
+class Zone extends Region
+{
+    public static function zonez0()
+    {
+        return parent::regionHuadong();
+    }
+
+    public static function zonez1()
+    {
+        return parent::regionHuabei();
+    }
+
+    public static function zonez2()
+    {
+        return parent::regionHuanan();
+    }
+
+    public static function zoneAs0()
+    {
+        return parent::regionSingapore();
+    }
+
+    public static function zoneNa0()
+    {
+        return parent::regionNorthAmerica();
+    }
+
+    public static function qvmZonez0()
+    {
+        return parent::qvmRegionHuadong();
+    }
+
+    public static function qvmZonez1()
+    {
+        return parent::qvmRegionHuabei();
+    }
+
+    public static function queryZone($ak, $bucket)
+    {
+        return parent::queryRegion($ak, $bucket);
+    }
+}

+ 264 - 0
addons/qiniu/library/Qiniu/functions.php

@@ -0,0 +1,264 @@
+<?php
+
+namespace Qiniu;
+
+use Qiniu\Config;
+
+if (!defined('QINIU_FUNCTIONS_VERSION')) {
+    define('QINIU_FUNCTIONS_VERSION', Config::SDK_VER);
+
+    /**
+     * 计算文件的crc32检验码:
+     *
+     * @param $file string  待计算校验码的文件路径
+     *
+     * @return string 文件内容的crc32校验码
+     */
+    function crc32_file($file)
+    {
+        $hash = hash_file('crc32b', $file);
+        $array = unpack('N', pack('H*', $hash));
+        return sprintf('%u', $array[1]);
+    }
+
+    /**
+     * 计算输入流的crc32检验码
+     *
+     * @param $data 待计算校验码的字符串
+     *
+     * @return string 输入字符串的crc32校验码
+     */
+    function crc32_data($data)
+    {
+        $hash = hash('crc32b', $data);
+        $array = unpack('N', pack('H*', $hash));
+        return sprintf('%u', $array[1]);
+    }
+
+    /**
+     * 对提供的数据进行urlsafe的base64编码。
+     *
+     * @param string $data 待编码的数据,一般为字符串
+     *
+     * @return string 编码后的字符串
+     * @link http://developer.qiniu.com/docs/v6/api/overview/appendix.html#urlsafe-base64
+     */
+    function base64_urlSafeEncode($data)
+    {
+        $find = array('+', '/');
+        $replace = array('-', '_');
+        return str_replace($find, $replace, base64_encode($data));
+    }
+
+    /**
+     * 对提供的urlsafe的base64编码的数据进行解码
+     *
+     * @param string $str 待解码的数据,一般为字符串
+     *
+     * @return string 解码后的字符串
+     */
+    function base64_urlSafeDecode($str)
+    {
+        $find = array('-', '_');
+        $replace = array('+', '/');
+        return base64_decode(str_replace($find, $replace, $str));
+    }
+
+    /**
+     * Wrapper for JSON decode that implements error detection with helpful
+     * error messages.
+     *
+     * @param string $json JSON data to parse
+     * @param bool $assoc When true, returned objects will be converted
+     *                        into associative arrays.
+     * @param int $depth User specified recursion depth.
+     *
+     * @return mixed
+     * @throws \InvalidArgumentException if the JSON cannot be parsed.
+     * @link http://www.php.net/manual/en/function.json-decode.php
+     */
+    function json_decode($json, $assoc = false, $depth = 512)
+    {
+        static $jsonErrors = array(
+            JSON_ERROR_DEPTH => 'JSON_ERROR_DEPTH - Maximum stack depth exceeded',
+            JSON_ERROR_STATE_MISMATCH => 'JSON_ERROR_STATE_MISMATCH - Underflow or the modes mismatch',
+            JSON_ERROR_CTRL_CHAR => 'JSON_ERROR_CTRL_CHAR - Unexpected control character found',
+            JSON_ERROR_SYNTAX => 'JSON_ERROR_SYNTAX - Syntax error, malformed JSON',
+            JSON_ERROR_UTF8 => 'JSON_ERROR_UTF8 - Malformed UTF-8 characters, possibly incorrectly encoded'
+        );
+
+        if (empty($json)) {
+            return null;
+        }
+        $data = \json_decode($json, $assoc, $depth);
+
+        if (JSON_ERROR_NONE !== json_last_error()) {
+            $last = json_last_error();
+            throw new \InvalidArgumentException(
+                'Unable to parse JSON data: '
+                . (isset($jsonErrors[$last])
+                    ? $jsonErrors[$last]
+                    : 'Unknown error')
+            );
+        }
+
+        return $data;
+    }
+
+    /**
+     * 计算七牛API中的数据格式
+     *
+     * @param $bucket 待操作的空间名
+     * @param $key 待操作的文件名
+     *
+     * @return string  符合七牛API规格的数据格式
+     * @link http://developer.qiniu.com/docs/v6/api/reference/data-formats.html
+     */
+    function entry($bucket, $key)
+    {
+        $en = $bucket;
+        if (!empty($key)) {
+            $en = $bucket . ':' . $key;
+        }
+        return base64_urlSafeEncode($en);
+    }
+
+    /**
+     * array 辅助方法,无值时不set
+     *
+     * @param $array 待操作array
+     * @param $key key
+     * @param $value value 为null时 不设置
+     *
+     * @return array 原来的array,便于连续操作
+     */
+    function setWithoutEmpty(&$array, $key, $value)
+    {
+        if (!empty($value)) {
+            $array[$key] = $value;
+        }
+        return $array;
+    }
+
+    /**
+     * 缩略图链接拼接
+     *
+     * @param  string $url 图片链接
+     * @param  int $mode 缩略模式
+     * @param  int $width 宽度
+     * @param  int $height 长度
+     * @param  string $format 输出类型
+     * @param  int $quality 图片质量
+     * @param  int $interlace 是否支持渐进显示
+     * @param  int $ignoreError 忽略结果
+     * @return string
+     * @link http://developer.qiniu.com/code/v6/api/kodo-api/image/imageview2.html
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    function thumbnail(
+        $url,
+        $mode,
+        $width,
+        $height,
+        $format = null,
+        $quality = null,
+        $interlace = null,
+        $ignoreError = 1
+    ) {
+
+        static $imageUrlBuilder = null;
+        if (is_null($imageUrlBuilder)) {
+            $imageUrlBuilder = new \Qiniu\Processing\ImageUrlBuilder;
+        }
+
+        return call_user_func_array(array($imageUrlBuilder, 'thumbnail'), func_get_args());
+    }
+
+    /**
+     * 图片水印
+     *
+     * @param  string $url 图片链接
+     * @param  string $image 水印图片链接
+     * @param  numeric $dissolve 透明度
+     * @param  string $gravity 水印位置
+     * @param  numeric $dx 横轴边距
+     * @param  numeric $dy 纵轴边距
+     * @param  numeric $watermarkScale 自适应原图的短边比例
+     * @link   http://developer.qiniu.com/code/v6/api/kodo-api/image/watermark.html
+     * @return string
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    function waterImg(
+        $url,
+        $image,
+        $dissolve = 100,
+        $gravity = 'SouthEast',
+        $dx = null,
+        $dy = null,
+        $watermarkScale = null
+    ) {
+
+        static $imageUrlBuilder = null;
+        if (is_null($imageUrlBuilder)) {
+            $imageUrlBuilder = new \Qiniu\Processing\ImageUrlBuilder;
+        }
+
+        return call_user_func_array(array($imageUrlBuilder, 'waterImg'), func_get_args());
+    }
+
+    /**
+     * 文字水印
+     *
+     * @param  string $url 图片链接
+     * @param  string $text 文字
+     * @param  string $font 文字字体
+     * @param  string $fontSize 文字字号
+     * @param  string $fontColor 文字颜色
+     * @param  numeric $dissolve 透明度
+     * @param  string $gravity 水印位置
+     * @param  numeric $dx 横轴边距
+     * @param  numeric $dy 纵轴边距
+     * @link   http://developer.qiniu.com/code/v6/api/kodo-api/image/watermark.html#text-watermark
+     * @return string
+     * @author Sherlock Ren <sherlock_ren@icloud.com>
+     */
+    function waterText(
+        $url,
+        $text,
+        $font = '黑体',
+        $fontSize = 0,
+        $fontColor = null,
+        $dissolve = 100,
+        $gravity = 'SouthEast',
+        $dx = null,
+        $dy = null
+    ) {
+
+        static $imageUrlBuilder = null;
+        if (is_null($imageUrlBuilder)) {
+            $imageUrlBuilder = new \Qiniu\Processing\ImageUrlBuilder;
+        }
+
+        return call_user_func_array(array($imageUrlBuilder, 'waterText'), func_get_args());
+    }
+
+    /**
+     *  从uptoken解析accessKey和bucket
+     *
+     * @param $upToken
+     * @return array(ak,bucket,err=null)
+     */
+    function explodeUpToken($upToken)
+    {
+        $items = explode(':', $upToken);
+        if (count($items) != 3) {
+            return array(null, null, "invalid uptoken");
+        }
+        $accessKey = $items[0];
+        $putPolicy = json_decode(base64_urlSafeDecode($items[2]));
+        $scope = $putPolicy->scope;
+        $scopeItems = explode(':', $scope);
+        $bucket = $scopeItems[0];
+        return array($accessKey, $bucket, null);
+    }
+}

Some files were not shown because too many files changed in this diff