rabin 1 year ago
parent
commit
683f9ff0d7

+ 107 - 39
sdk/Qiniu/Auth.php

@@ -1,17 +1,26 @@
 <?php
 namespace Qiniu;
 
+use Qiniu\Http\Header;
 use Qiniu\Zone;
 
 final class Auth
 {
     private $accessKey;
     private $secretKey;
+    public $options;
 
-    public function __construct($accessKey, $secretKey)
+    public function __construct($accessKey, $secretKey, $options = null)
     {
         $this->accessKey = $accessKey;
         $this->secretKey = $secretKey;
+        $defaultOptions = array(
+            'disableQiniuTimestampSignature' => null
+        );
+        if ($options == null) {
+            $options = $defaultOptions;
+        }
+        $this->options = array_merge($defaultOptions, $options);
     }
 
     public function getAccessKey()
@@ -49,6 +58,81 @@ final class Auth
         return $this->sign($data);
     }
 
+    /**
+     * @param string $urlString
+     * @param string $method
+     * @param string $body
+     * @param null|Header $headers
+     */
+    public function signQiniuAuthorization($urlString, $method = "GET", $body = "", $headers = null)
+    {
+        $url = parse_url($urlString);
+        if (!$url) {
+            return array(null, new \Exception("parse_url error"));
+        }
+
+        // append method, path and query
+        if ($method === "") {
+            $data = "GET ";
+        } else {
+            $data = $method . " ";
+        }
+        if (isset($url["path"])) {
+            $data .= $url["path"];
+        }
+        if (isset($url["query"])) {
+            $data .= "?" . $url["query"];
+        }
+
+        // append Host
+        $data .= "\n";
+        $data .= "Host: ";
+        if (isset($url["host"])) {
+            $data .= $url["host"];
+        }
+        if (isset($url["port"]) && $url["port"] > 0) {
+            $data .= ":" . $url["port"];
+        }
+
+        // try append content type
+        if ($headers != null && isset($headers["Content-Type"])) {
+            // append content type
+            $data .= "\n";
+            $data .= "Content-Type: " . $headers["Content-Type"];
+        }
+
+        // try append xQiniuHeaders
+        if ($headers != null) {
+            $headerLines = array();
+            $keyPrefix = "X-Qiniu-";
+            foreach ($headers as $k => $v) {
+                if (strlen($k) > strlen($keyPrefix) && strpos($k, $keyPrefix) === 0) {
+                    array_push(
+                        $headerLines,
+                        $k . ": " . $v
+                    );
+                }
+            }
+            if (count($headerLines) > 0) {
+                $data .= "\n";
+                sort($headerLines);
+                $data .= implode("\n", $headerLines);
+            }
+        }
+
+        // append body
+        $data .= "\n\n";
+        if (!is_null($body)
+            && strlen($body) > 0
+            && isset($headers["Content-Type"])
+            && $headers["Content-Type"] != "application/octet-stream"
+        ) {
+            $data .= $body;
+        }
+
+        return array($this->sign($data), null);
+    }
+
     public function verifyCallback($contentType, $originAuthorization, $url, $body)
     {
         $authorization = 'QBox ' . $this->signRequest($url, $body, $contentType);
@@ -103,6 +187,7 @@ final class Auth
 
         'endUser',
         'saveKey',
+        'forceSaveKey',
         'insertOnly',
 
         'detectMime',
@@ -140,48 +225,31 @@ final class Auth
 
     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 = '';
+        $headers = new Header();
+        $result = array();
+        if ($contentType != null) {
+            $headers['Content-Type'] = $contentType;
+            $result['Content-Type'] = $contentType;
         }
 
-        $path = $urlItems['path'];
-        if (isset($urlItems['query'])) {
-            $query = $urlItems['query'];
+        $signDate = gmdate('Ymd\THis\Z', time());
+        if ($this->options['disableQiniuTimestampSignature'] !== null) {
+            if (!$this->options['disableQiniuTimestampSignature']) {
+                $headers['X-Qiniu-Date'] = $signDate;
+                $result['X-Qiniu-Date'] = $signDate;
+            }
+        } elseif (getenv("DISABLE_QINIU_TIMESTAMP_SIGNATURE")) {
+            if (strtolower(getenv("DISABLE_QINIU_TIMESTAMP_SIGNATURE")) !== "true") {
+                $headers['X-Qiniu-Date'] = $signDate;
+                $result['X-Qiniu-Date'] = $signDate;
+            }
         } 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;
+            $headers['X-Qiniu-Date'] = $signDate;
+            $result['X-Qiniu-Date'] = $signDate;
         }
 
-        $sign = $this->sign($toSignStr);
-        $auth = 'Qiniu ' . $sign;
-        return array('Authorization' => $auth);
+        list($sign) = $this->signQiniuAuthorization($url, $method, $body, $headers);
+        $result['Authorization'] = 'Qiniu ' . $sign;
+        return $result;
     }
 }

+ 80 - 7
sdk/Qiniu/Cdn/CdnManager.php

@@ -5,17 +5,20 @@ namespace Qiniu\Cdn;
 use Qiniu\Auth;
 use Qiniu\Http\Error;
 use Qiniu\Http\Client;
+use Qiniu\Http\Proxy;
 
 final class CdnManager
 {
 
     private $auth;
     private $server;
+    private $proxy;
 
-    public function __construct(Auth $auth)
+    public function __construct(Auth $auth, $proxy = null, $proxy_auth = null, $proxy_user_password = null)
     {
         $this->auth = $auth;
         $this->server = 'http://fusion.qiniuapi.com';
+        $this->proxy = new Proxy($proxy, $proxy_auth, $proxy_user_password);
     }
 
     /**
@@ -63,6 +66,45 @@ final class CdnManager
         return $this->post($url, $body);
     }
 
+    /**
+     * 查询 CDN 刷新记录
+     *
+     * @param string $requestId 指定要查询记录所在的刷新请求id
+     * @param string $isDir 指定是否查询目录,取值为 yes/no,默认不填则为两种类型记录都查询
+     * @param array $urls 要查询的url列表,每个url可以是文件url,也可以是目录url
+     * @param string $state 指定要查询记录的状态,取值processing/success/failure
+     * @param int $pageNo 要求返回的页号,默认为0
+     * @param int $pageSize 要求返回的页长度,默认为100
+     * @param string $startTime 指定查询的开始日期,格式2006-01-01
+     * @param string $endTime 指定查询的结束日期,格式2006-01-01
+     * @return array
+     * @link https://developer.qiniu.com/fusion/api/1229/cache-refresh#4
+     */
+    public function getCdnRefreshList(
+        $requestId = null,
+        $isDir = null,
+        $urls = array(),
+        $state = null,
+        $pageNo = 0,
+        $pageSize = 100,
+        $startTime = null,
+        $endTime = null
+    ) {
+        $req = array();
+        \Qiniu\setWithoutEmpty($req, 'requestId', $requestId);
+        \Qiniu\setWithoutEmpty($req, 'isDir', $isDir);
+        \Qiniu\setWithoutEmpty($req, 'urls', $urls);
+        \Qiniu\setWithoutEmpty($req, 'state', $state);
+        \Qiniu\setWithoutEmpty($req, 'pageNo', $pageNo);
+        \Qiniu\setWithoutEmpty($req, 'pageSize', $pageSize);
+        \Qiniu\setWithoutEmpty($req, 'startTime', $startTime);
+        \Qiniu\setWithoutEmpty($req, 'endTime', $endTime);
+
+        $body = json_encode($req);
+        $url = $this->server . '/v2/tune/refresh/list';
+        return $this->post($url, $body);
+    }
+
     /**
      * @param array $urls 待预取的文件链接数组
      *
@@ -81,6 +123,42 @@ final class CdnManager
         return $this->post($url, $body);
     }
 
+    /**
+     * 查询 CDN 预取记录
+     *
+     * @param string $requestId 指定要查询记录所在的刷新请求id
+     * @param array $urls 要查询的url列表,每个url可以是文件url,也可以是目录url
+     * @param string $state 指定要查询记录的状态,取值processing/success/failure
+     * @param int $pageNo 要求返回的页号,默认为0
+     * @param int $pageSize 要求返回的页长度,默认为100
+     * @param string $startTime 指定查询的开始日期,格式2006-01-01
+     * @param string $endTime 指定查询的结束日期,格式2006-01-01
+     * @return array
+     * @link https://developer.qiniu.com/fusion/api/1227/file-prefetching#4
+     */
+    public function getCdnPrefetchList(
+        $requestId = null,
+        $urls = array(),
+        $state = null,
+        $pageNo = 0,
+        $pageSize = 100,
+        $startTime = null,
+        $endTime = null
+    ) {
+        $req = array();
+        \Qiniu\setWithoutEmpty($req, 'requestId', $requestId);
+        \Qiniu\setWithoutEmpty($req, 'urls', $urls);
+        \Qiniu\setWithoutEmpty($req, 'state', $state);
+        \Qiniu\setWithoutEmpty($req, 'pageNo', $pageNo);
+        \Qiniu\setWithoutEmpty($req, 'pageSize', $pageSize);
+        \Qiniu\setWithoutEmpty($req, 'startTime', $startTime);
+        \Qiniu\setWithoutEmpty($req, 'endTime', $endTime);
+
+        $body = json_encode($req);
+        $url = $this->server . '/v2/tune/prefetch/list';
+        return $this->post($url, $body);
+    }
+
     /**
      * @param array $domains 待获取带宽数据的域名数组
      * @param string $startDate 开始的日期,格式类似 2017-01-01
@@ -150,7 +228,7 @@ final class CdnManager
     {
         $headers = $this->auth->authorization($url, $body, 'application/json');
         $headers['Content-Type'] = 'application/json';
-        $ret = Client::post($url, $body, $headers);
+        $ret = Client::post($url, $body, $headers, $this->proxy->makeReqOpt());
         if (!$ret->ok()) {
             return array(null, new Error($url, $ret));
         }
@@ -169,22 +247,17 @@ final class CdnManager
      */
     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;
     }
 }

+ 304 - 40
sdk/Qiniu/Config.php

@@ -1,72 +1,184 @@
 <?php
+
 namespace Qiniu;
 
 final class Config
 {
-    const SDK_VER = '7.2.3';
+    const SDK_VER = '7.12.0';
 
     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 = 'https://api.qiniu.com';              //UC Host
+    const RSF_HOST = 'rsf.qiniuapi.com';
+    const API_HOST = 'api.qiniuapi.com';
+    const RS_HOST = 'rs.qiniuapi.com';      //RS Host
+    const UC_HOST = 'uc.qbox.me';              //UC Host
+    const QUERY_REGION_HOST = 'kodo-config.qiniuapi.com';
+    const RTCAPI_HOST = 'http://rtc.qiniuapi.com';
+    const ARGUS_HOST = 'ai.qiniuapi.com';
+    const CASTER_HOST = 'pili-caster.qiniuapi.com';
+    const SMS_HOST = "https://sms.qiniuapi.com";
+    const RTCAPI_VERSION = 'v3';
+    const SMS_VERSION = 'v1';
 
-    // Zone 空间对应的机房
-    public $zone;
+    // Zone 空间对应的存储区域
+    public $region;
     //BOOL 是否使用https域名
     public $useHTTPS;
     //BOOL 是否使用CDN加速上传域名
     public $useCdnDomains;
+    /**
+     * @var Region
+     */
+    public $zone;
     // Zone Cache
-    private $zoneCache;
+    private $regionCache;
+    // UC Host
+    private $ucHost;
+    private $queryRegionHost;
+    // backup UC Hosts
+    private $backupQueryRegionHosts;
+    // backup UC Hosts max retry time
+    public $backupUcHostsRetryTimes;
 
     // 构造函数
-    public function __construct(Zone $z = null)
+    public function __construct(Region $z = null)
     {
         $this->zone = $z;
         $this->useHTTPS = false;
         $this->useCdnDomains = false;
-        $this->zoneCache = array();
+        $this->regionCache = array();
+        $this->ucHost = Config::UC_HOST;
+        $this->queryRegionHost = Config::QUERY_REGION_HOST;
+        $this->backupQueryRegionHosts = array(
+            "uc.qbox.me",
+            "api.qiniu.com"
+        );
+        $this->backupUcHostsRetryTimes = 2;
+    }
+
+    public function setUcHost($ucHost)
+    {
+        $this->ucHost = $ucHost;
+        $this->setQueryRegionHost($ucHost);
+    }
+
+    public function getUcHost()
+    {
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return $scheme . $this->ucHost;
+    }
+
+    public function setQueryRegionHost($host, $backupHosts = array())
+    {
+        $this->queryRegionHost = $host;
+        $this->backupQueryRegionHosts = $backupHosts;
+    }
+
+    public function getQueryRegionHost()
+    {
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return $scheme . $this->queryRegionHost;
+    }
+
+    public function setBackupQueryRegionHosts($hosts = array())
+    {
+        $this->backupQueryRegionHosts = $hosts;
+    }
+
+    public function getBackupQueryRegionHosts()
+    {
+        return $this->backupQueryRegionHosts;
     }
 
-    public function getUpHost($accessKey, $bucket)
+    public function getUpHost($accessKey, $bucket, $reqOpt = null)
     {
-        $zone = $this->getZone($accessKey, $bucket);
+        $region = $this->getRegion($accessKey, $bucket, $reqOpt);
         if ($this->useHTTPS === true) {
             $scheme = "https://";
         } else {
             $scheme = "http://";
         }
 
-        $host = $zone->srcUpHosts[0];
+        $host = $region->srcUpHosts[0];
         if ($this->useCdnDomains === true) {
-            $host = $zone->cdnUpHosts[0];
+            $host = $region->cdnUpHosts[0];
         }
 
         return $scheme . $host;
     }
 
-    public function getUpBackupHost($accessKey, $bucket)
+    public function getUpHostV2($accessKey, $bucket, $reqOpt = null)
     {
-        $zone = $this->getZone($accessKey, $bucket);
+        list($region, $err) = $this->getRegionV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        $host = $region->srcUpHosts[0];
+        if ($this->useCdnDomains === true) {
+            $host = $region->cdnUpHosts[0];
+        }
+
+        return array($scheme . $host, null);
+    }
+
+    public function getUpBackupHost($accessKey, $bucket, $reqOpt = null)
+    {
+        $region = $this->getRegion($accessKey, $bucket, $reqOpt);
         if ($this->useHTTPS === true) {
             $scheme = "https://";
         } else {
             $scheme = "http://";
         }
 
-        $host = $zone->cdnUpHosts[0];
+        $host = $region->cdnUpHosts[0];
         if ($this->useCdnDomains === true) {
-            $host = $zone->srcUpHosts[0];
+            $host = $region->srcUpHosts[0];
         }
 
         return $scheme . $host;
     }
 
-    public function getRsHost($accessKey, $bucket)
+    public function getUpBackupHostV2($accessKey, $bucket, $reqOpt = null)
+    {
+        list($region, $err) = $this->getRegionV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        $host = $region->cdnUpHosts[0];
+        if ($this->useCdnDomains === true) {
+            $host = $region->srcUpHosts[0];
+        }
+
+        return array($scheme . $host, null);
+    }
+
+    public function getRsHost($accessKey, $bucket, $reqOpt = null)
     {
-        $zone = $this->getZone($accessKey, $bucket);
+        $region = $this->getRegion($accessKey, $bucket, $reqOpt);
 
         if ($this->useHTTPS === true) {
             $scheme = "https://";
@@ -74,12 +186,15 @@ final class Config
             $scheme = "http://";
         }
 
-        return $scheme . $zone->rsHost;
+        return $scheme . $region->rsHost;
     }
 
-    public function getRsfHost($accessKey, $bucket)
+    public function getRsHostV2($accessKey, $bucket, $reqOpt = null)
     {
-        $zone = $this->getZone($accessKey, $bucket);
+        list($region, $err) = $this->getRegionV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
 
         if ($this->useHTTPS === true) {
             $scheme = "https://";
@@ -87,12 +202,12 @@ final class Config
             $scheme = "http://";
         }
 
-        return $scheme . $zone->rsfHost;
+        return array($scheme . $region->rsHost, null);
     }
 
-    public function getIovipHost($accessKey, $bucket)
+    public function getRsfHost($accessKey, $bucket, $reqOpt = null)
     {
-        $zone = $this->getZone($accessKey, $bucket);
+        $region = $this->getRegion($accessKey, $bucket, $reqOpt);
 
         if ($this->useHTTPS === true) {
             $scheme = "https://";
@@ -100,12 +215,15 @@ final class Config
             $scheme = "http://";
         }
 
-        return $scheme . $zone->iovipHost;
+        return $scheme . $region->rsfHost;
     }
 
-    public function getApiHost($accessKey, $bucket)
+    public function getRsfHostV2($accessKey, $bucket, $reqOpt = null)
     {
-        $zone = $this->getZone($accessKey, $bucket);
+        list($region, $err) = $this->getRegionV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
 
         if ($this->useHTTPS === true) {
             $scheme = "https://";
@@ -113,22 +231,168 @@ final class Config
             $scheme = "http://";
         }
 
-        return $scheme . $zone->apiHost;
+        return array($scheme . $region->rsfHost, null);
     }
 
-    private function getZone($accessKey, $bucket)
+    public function getIovipHost($accessKey, $bucket, $reqOpt = null)
     {
-        $cacheId = "$accessKey:$bucket";
+        $region = $this->getRegion($accessKey, $bucket, $reqOpt);
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
 
-        if (isset($this->zoneCache[$cacheId])) {
-            $zone = $this->zoneCache[$cacheId];
-        } elseif (isset($this->zone)) {
-            $zone = $this->zone;
-            $this->zoneCache[$cacheId] = $zone;
+        return $scheme . $region->iovipHost;
+    }
+
+    public function getIovipHostV2($accessKey, $bucket, $reqOpt = null)
+    {
+        list($region, $err) = $this->getRegionV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
         } else {
-            $zone = Zone::queryZone($accessKey, $bucket);
-            $this->zoneCache[$cacheId] = $zone;
+            $scheme = "http://";
         }
-        return $zone;
+
+        return array($scheme . $region->iovipHost, null);
+    }
+
+    public function getApiHost($accessKey, $bucket, $reqOpt = null)
+    {
+        $region = $this->getRegion($accessKey, $bucket, $reqOpt);
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return $scheme . $region->apiHost;
+    }
+
+    public function getApiHostV2($accessKey, $bucket, $reqOpt = null)
+    {
+        list($region, $err) = $this->getRegionV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        if ($this->useHTTPS === true) {
+            $scheme = "https://";
+        } else {
+            $scheme = "http://";
+        }
+
+        return array($scheme . $region->apiHost, null);
+    }
+
+
+    /**
+     * 从缓存中获取区域
+     *
+     * @param string $cacheId 缓存 ID
+     * @return null|Region
+     */
+    private function getRegionCache($cacheId)
+    {
+        if (isset($this->regionCache[$cacheId]) &&
+            isset($this->regionCache[$cacheId]["deadline"]) &&
+            time() < $this->regionCache[$cacheId]["deadline"]) {
+            return $this->regionCache[$cacheId]["region"];
+        }
+
+        return null;
+    }
+
+    /**
+     * 将区域设置到缓存中
+     *
+     * @param string $cacheId 缓存 ID
+     * @param Region $region 缓存 ID
+     * @return void
+     */
+    private function setRegionCache($cacheId, $region)
+    {
+        $this->regionCache[$cacheId] = array(
+            "region" => $region,
+        );
+        if (isset($region->ttl)) {
+            $this->regionCache[$cacheId]["deadline"] = time() + $region->ttl;
+        }
+    }
+
+    /**
+     * 从缓存中获取区域
+     *
+     * @param string $accessKey
+     * @param string $bucket
+     * @return Region
+     *
+     * @throws \Exception
+     */
+    private function getRegion($accessKey, $bucket, $reqOpt = null)
+    {
+        if (isset($this->zone)) {
+            return $this->zone;
+        }
+
+        $cacheId = "$accessKey:$bucket";
+        $regionCache = $this->getRegionCache($cacheId);
+        if ($regionCache) {
+            return $regionCache;
+        }
+
+        $region = Zone::queryZone(
+            $accessKey,
+            $bucket,
+            $this->getQueryRegionHost(),
+            $this->getBackupQueryRegionHosts(),
+            $this->backupUcHostsRetryTimes,
+            $reqOpt
+        );
+        if (is_array($region)) {
+            list($region, $err) = $region;
+            if ($err != null) {
+                throw new \Exception($err->message());
+            }
+        }
+
+        $this->setRegionCache($cacheId, $region);
+        return $region;
+    }
+
+    private function getRegionV2($accessKey, $bucket, $reqOpt = null)
+    {
+        if (isset($this->zone)) {
+            return array($this->zone, null);
+        }
+
+        $cacheId = "$accessKey:$bucket";
+        $regionCache = $this->getRegionCache($cacheId);
+        if (isset($regionCache)) {
+            return array($regionCache, null);
+        }
+
+        $region = Zone::queryZone(
+            $accessKey,
+            $bucket,
+            $this->getQueryRegionHost(),
+            $this->getBackupQueryRegionHosts(),
+            $this->backupUcHostsRetryTimes,
+            $reqOpt
+        );
+        if (is_array($region)) {
+            list($region, $err) = $region;
+            return array($region, $err);
+        }
+
+        $this->setRegionCache($cacheId, $region);
+        return array($region, null);
     }
 }

+ 53 - 0
sdk/Qiniu/Enum/QiniuEnum.php

@@ -0,0 +1,53 @@
+<?php
+// @codingStandardsIgnoreStart
+// phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
+
+namespace Qiniu\Enum;
+
+use MyCLabs\Enum\Enum;
+
+if (method_exists("MyCLabs\\Enum\\Enum", "from")) {
+    abstract class QiniuEnum extends Enum
+    {
+        // @codingStandardsIgnoreEnd
+        // @codingStandardsIgnoreStart
+    }
+} else {
+    /**
+     * poly fill MyCLabs\Enum\Enum::from in low version
+     *
+     * @link https://github.com/myclabs/php-enum
+     */
+    abstract class QiniuEnum extends Enum
+    {
+        // @codingStandardsIgnoreEnd
+        /**
+         * @param mixed $value
+         * @return static
+         */
+        public static function from($value)
+        {
+            $key = self::assertValidValueReturningKey($value);
+
+            return self::__callStatic($key, array());
+        }
+
+        /**
+         * Asserts valid enum value
+         *
+         * @psalm-pure
+         * @psalm-assert T $value
+         * @param mixed $value
+         * @return string
+         */
+        private static function assertValidValueReturningKey($value)
+        {
+            if (false === ($key = self::search($value))) {
+                throw new \UnexpectedValueException("Value '$value' is not part of the enum " . __CLASS__);
+            }
+
+            return $key;
+        }
+        // @codingStandardsIgnoreStart
+    }
+}

+ 9 - 0
sdk/Qiniu/Enum/SplitUploadVersion.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Qiniu\Enum;
+
+final class SplitUploadVersion extends QiniuEnum
+{
+    const V1 = 'v1';
+    const V2 = 'v2';
+}

+ 82 - 29
sdk/Qiniu/Http/Client.php

@@ -1,24 +1,73 @@
 <?php
+
 namespace Qiniu\Http;
 
 use Qiniu\Config;
-use Qiniu\Http\Request;
-use Qiniu\Http\Response;
+use Qiniu\Http\Middleware;
 
 final class Client
 {
-    public static function get($url, array $headers = array())
+    /**
+     * @param $url
+     * @param array $headers
+     * @param RequestOptions $opt
+     * @return Response
+     */
+    public static function get($url, array $headers = array(), $opt = null)
+    {
+        $request = new Request('GET', $url, $headers, null, $opt);
+        return self::sendRequestWithMiddleware($request);
+    }
+
+    /**
+     * @param $url
+     * @param array $headers
+     * @param array $opt detail see {@see Request::$opt}
+     * @return Response
+     */
+    public static function delete($url, array $headers = array(), $opt = null)
     {
-        $request = new Request('GET', $url, $headers);
+        $request = new Request('DELETE', $url, $headers, null, $opt);
         return self::sendRequest($request);
     }
 
-    public static function post($url, $body, array $headers = array())
+    /**
+     * @param $url
+     * @param $body
+     * @param array $headers
+     * @param RequestOptions $opt
+     * @return Response
+     */
+    public static function post($url, $body, array $headers = array(), $opt = null)
     {
-        $request = new Request('POST', $url, $headers, $body);
+        $request = new Request('POST', $url, $headers, $body, $opt);
         return self::sendRequest($request);
     }
 
+    /**
+     * @param $url
+     * @param $body
+     * @param array $headers
+     * @param RequestOptions $opt
+     * @return Response
+     */
+    public static function PUT($url, $body, array $headers = array(), $opt = null)
+    {
+        $request = new Request('PUT', $url, $headers, $body, $opt);
+        return self::sendRequest($request);
+    }
+
+    /**
+     * @param $url
+     * @param array $fields
+     * @param string $name
+     * @param string $fileName
+     * @param $fileBody
+     * @param null $mimeType
+     * @param array $headers
+     * @param RequestOptions $opt
+     * @return Response
+     */
     public static function multipartPost(
         $url,
         $fields,
@@ -26,7 +75,8 @@ final class Client
         $fileName,
         $fileBody,
         $mimeType = null,
-        array $headers = array()
+        $headers = array(),
+        $opt = null
     ) {
         $data = array();
         $mimeBoundary = md5(microtime());
@@ -52,7 +102,7 @@ final class Client
         $body = implode("\r\n", $data);
         $contentType = 'multipart/form-data; boundary=' . $mimeBoundary;
         $headers['Content-Type'] = $contentType;
-        $request = new Request('POST', $url, $headers, $body);
+        $request = new Request('POST', $url, $headers, $body, $opt);
         return self::sendRequest($request);
     }
 
@@ -71,6 +121,23 @@ final class Client
         return $ua;
     }
 
+    /**
+     * @param Request $request
+     * @return Response
+     */
+    public static function sendRequestWithMiddleware($request)
+    {
+        $middlewares = $request->opt->middlewares;
+        $handle = Middleware\compose($middlewares, function ($req) {
+            return Client::sendRequest($req);
+        });
+        return $handle($request);
+    }
+
+    /**
+     * @param Request $request
+     * @return Response
+     */
     public static function sendRequest($request)
     {
         $t1 = microtime(true);
@@ -78,19 +145,18 @@ final class Client
         $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,
         );
-
+        foreach ($request->opt->getCurlOpt() as $k => $v) {
+            $options[$k] = $v;
+        }
         // 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) {
@@ -99,7 +165,6 @@ final class Client
             $options[CURLOPT_HTTPHEADER] = $headers;
         }
         curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
-
         if (!empty($request->body)) {
             $options[CURLOPT_POSTFIELDS] = $request->body;
         }
@@ -115,29 +180,17 @@ final class Client
         }
         $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
         $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
-        $headers = self::parseHeaders(substr($result, 0, $header_size));
+        $headers = Header::parseRawText(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] = ucwords($kv[0], '-');
-                $headers[$kv[0]] = trim($kv[1]);
-            }
-        }
-        return $headers;
-    }
-
     private static function escapeQuotes($str)
     {
+        if (is_null($str)) {
+            return null;
+        }
         $find = array("\\", "\"");
         $replace = array("\\\\", "\\\"");
         return str_replace($find, $replace, $str);

+ 3 - 0
sdk/Qiniu/Http/Error.php

@@ -10,6 +10,9 @@ namespace Qiniu\Http;
 final class Error
 {
     private $url;
+    /**
+     * @var Response
+     */
     private $response;
 
     public function __construct($url, $response)

+ 281 - 0
sdk/Qiniu/Http/Header.php

@@ -0,0 +1,281 @@
+<?php
+
+namespace Qiniu\Http;
+
+/**
+ * field name case-insensitive Header
+ */
+class Header implements \ArrayAccess, \IteratorAggregate, \Countable
+{
+    /** @var array normalized key name map */
+    private $data = array();
+
+    /**
+     * @param array $obj non-normalized header object
+     */
+    public function __construct($obj = array())
+    {
+        foreach ($obj as $key => $values) {
+            $normalizedKey = self::normalizeKey($key);
+            $normalizedValues = array();
+            foreach ($values as $value) {
+                array_push($normalizedValues, self::normalizeValue($value));
+            }
+            $this->data[$normalizedKey] = $normalizedValues;
+        }
+        return $this;
+    }
+
+    /**
+     * return origin headers, which is field name case-sensitive
+     *
+     * @param string $raw
+     *
+     * @return array
+     */
+    public static function parseRawText($raw)
+    {
+        $multipleHeaders = explode("\r\n\r\n", trim($raw));
+        $headers = array();
+        $headerLines = explode("\r\n", end($multipleHeaders));
+        foreach ($headerLines as $line) {
+            $headerLine = trim($line);
+            $kv = explode(':', $headerLine);
+            if (count($kv) <= 1) {
+                continue;
+            }
+            // for http2 [Pseudo-Header Fields](https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.1)
+            if ($kv[0] == "") {
+                $fieldName = ":" . $kv[1];
+            } else {
+                $fieldName = $kv[0];
+            }
+            $fieldValue = trim(substr($headerLine, strlen($fieldName . ":")));
+            if (isset($headers[$fieldName])) {
+                array_push($headers[$fieldName], $fieldValue);
+            } else {
+                $headers[$fieldName] = array($fieldValue);
+            }
+        }
+        return $headers;
+    }
+
+    /**
+     * @param string $raw
+     *
+     * @return Header
+     */
+    public static function fromRawText($raw)
+    {
+        return new Header(self::parseRawText($raw));
+    }
+
+    /**
+     * @param string $key
+     *
+     * @return string
+     */
+    public static function normalizeKey($key)
+    {
+        $key = trim($key);
+
+        if (!self::isValidKeyName($key)) {
+            return $key;
+        }
+
+        return \Qiniu\ucwords(strtolower($key), '-');
+    }
+
+    /**
+     * @param string|numeric $value
+     *
+     * @return string|numeric
+     */
+    public static function normalizeValue($value)
+    {
+        if (is_numeric($value)) {
+            return $value + 0;
+        }
+        return trim($value);
+    }
+
+    /**
+     * @return array
+     */
+    public function getRawData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * @param $offset string
+     *
+     * @return boolean
+     */
+    #[\ReturnTypeWillChange] // temporarily suppress the type check of php 8.x
+    public function offsetExists($offset)
+    {
+        $key = self::normalizeKey($offset);
+        return isset($this->data[$key]);
+    }
+
+    /**
+     * @param $offset string
+     *
+     * @return string|null
+     */
+    #[\ReturnTypeWillChange] // temporarily suppress the type check of php 8.x
+    public function offsetGet($offset)
+    {
+        $key = self::normalizeKey($offset);
+        if (isset($this->data[$key]) && count($this->data[$key])) {
+            return $this->data[$key][0];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @param $offset string
+     * @param $value string
+     *
+     * @return void
+     */
+    #[\ReturnTypeWillChange] // temporarily suppress the type check of php 8.x
+    public function offsetSet($offset, $value)
+    {
+        $key = self::normalizeKey($offset);
+        if (isset($this->data[$key]) && count($this->data[$key]) > 0) {
+            $this->data[$key][0] = self::normalizeValue($value);
+        } else {
+            $this->data[$key] = array(self::normalizeValue($value));
+        }
+    }
+
+    /**
+     * @return void
+     */
+    #[\ReturnTypeWillChange] // temporarily suppress the type check of php 8.x
+    public function offsetUnset($offset)
+    {
+        $key = self::normalizeKey($offset);
+        unset($this->data[$key]);
+    }
+
+    /**
+     * @return \ArrayIterator
+     */
+    #[\ReturnTypeWillChange] // temporarily suppress the type check of php 8.x
+    public function getIterator()
+    {
+        $arr = array();
+        foreach ($this->data as $k => $v) {
+            $arr[$k] = $v[0];
+        }
+        return new \ArrayIterator($arr);
+    }
+
+    /**
+     * @return int
+     */
+    #[\ReturnTypeWillChange] // temporarily suppress the type check of php 8.x
+    public function count()
+    {
+        return count($this->data);
+    }
+
+    private static $isTokenTable = array(
+        '!' => true,
+        '#' => true,
+        '$' => true,
+        '%' => true,
+        '&' => true,
+        '\'' => true,
+        '*' => true,
+        '+' => true,
+        '-' => true,
+        '.' => true,
+        '0' => true,
+        '1' => true,
+        '2' => true,
+        '3' => true,
+        '4' => true,
+        '5' => true,
+        '6' => true,
+        '7' => true,
+        '8' => true,
+        '9' => true,
+        'A' => true,
+        'B' => true,
+        'C' => true,
+        'D' => true,
+        'E' => true,
+        'F' => true,
+        'G' => true,
+        'H' => true,
+        'I' => true,
+        'J' => true,
+        'K' => true,
+        'L' => true,
+        'M' => true,
+        'N' => true,
+        'O' => true,
+        'P' => true,
+        'Q' => true,
+        'R' => true,
+        'S' => true,
+        'T' => true,
+        'U' => true,
+        'W' => true,
+        'V' => true,
+        'X' => true,
+        'Y' => true,
+        'Z' => true,
+        '^' => true,
+        '_' => true,
+        '`' => true,
+        'a' => true,
+        'b' => true,
+        'c' => true,
+        'd' => true,
+        'e' => true,
+        'f' => true,
+        'g' => true,
+        'h' => true,
+        'i' => true,
+        'j' => true,
+        'k' => true,
+        'l' => true,
+        'm' => true,
+        'n' => true,
+        'o' => true,
+        'p' => true,
+        'q' => true,
+        'r' => true,
+        's' => true,
+        't' => true,
+        'u' => true,
+        'v' => true,
+        'w' => true,
+        'x' => true,
+        'y' => true,
+        'z' => true,
+        '|' => true,
+        '~' => true,
+    );
+
+    /**
+     * @param string $str
+     *
+     * @return boolean
+     */
+    private static function isValidKeyName($str)
+    {
+        for ($i = 0; $i < strlen($str); $i += 1) {
+            if (!isset(self::$isTokenTable[$str[$i]])) {
+                return false;
+            }
+        }
+        return true;
+    }
+}

+ 31 - 0
sdk/Qiniu/Http/Middleware/Middleware.php

@@ -0,0 +1,31 @@
+<?php
+namespace Qiniu\Http\Middleware;
+
+use Qiniu\Http\Request;
+use Qiniu\Http\Response;
+
+interface Middleware
+{
+    /**
+     * @param Request $request
+     * @param callable(Request): Response $next
+     * @return Response
+     */
+    public function send($request, $next);
+}
+
+/**
+ * @param array<Middleware> $middlewares
+ * @param callable(Request): Response $handler
+ * @return callable(Request): Response
+ */
+function compose($middlewares, $handler)
+{
+    $next = $handler;
+    foreach (array_reverse($middlewares) as $middleware) {
+        $next = function ($request) use ($middleware, $next) {
+            return $middleware->send($request, $next);
+        };
+    }
+    return $next;
+}

+ 76 - 0
sdk/Qiniu/Http/Middleware/RetryDomainsMiddleware.php

@@ -0,0 +1,76 @@
+<?php
+namespace Qiniu\Http\Middleware;
+
+use Qiniu\Http\Request;
+use Qiniu\Http\Response;
+
+class RetryDomainsMiddleware implements Middleware
+{
+    /**
+     * @var array<string> backup domains.
+     */
+    private $backupDomains;
+
+    /**
+     * @var numeric max retry times for each backup domains.
+     */
+    private $maxRetryTimes;
+
+    /**
+     * @var callable args response and request; returns bool; If true will retry with backup domains.
+     */
+    private $retryCondition;
+
+    /**
+     * @param array<string> $backupDomains
+     * @param numeric $maxRetryTimes
+     */
+    public function __construct($backupDomains, $maxRetryTimes = 2, $retryCondition = null)
+    {
+        $this->backupDomains = $backupDomains;
+        $this->maxRetryTimes = $maxRetryTimes;
+        $this->retryCondition = $retryCondition;
+    }
+
+    private function shouldRetry($resp, $req)
+    {
+        if (is_callable($this->retryCondition)) {
+            return call_user_func($this->retryCondition, $resp, $req);
+        }
+
+        return !$resp || $resp->needRetry();
+    }
+
+    /**
+     * @param Request $request
+     * @param callable(Request): Response $next
+     * @return Response
+     */
+    public function send($request, $next)
+    {
+        $response = null;
+        $urlComponents = parse_url($request->url);
+
+        foreach (array_merge(array($urlComponents["host"]), $this->backupDomains) as $backupDomain) {
+            $urlComponents["host"] = $backupDomain;
+            $request->url = \Qiniu\unparse_url($urlComponents);
+            $retriedTimes = 0;
+
+            while ($retriedTimes < $this->maxRetryTimes) {
+                $response = $next($request);
+
+                $retriedTimes += 1;
+
+                if (!$this->shouldRetry($response, $request)) {
+                    return $response;
+                }
+            }
+        }
+
+        if (!$response) {
+            $response = $next($request);
+        }
+
+        return $response;
+    }
+}

+ 34 - 0
sdk/Qiniu/Http/Proxy.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Qiniu\Http;
+
+use Qiniu\Http\RequestOptions;
+
+final class Proxy
+{
+    private $proxy;
+    private $proxy_auth;
+    private $proxy_user_password;
+
+    public function __construct($proxy = null, $proxy_auth = null, $proxy_user_password = null)
+    {
+        $this->proxy = $proxy;
+        $this->proxy_auth = $proxy_auth;
+        $this->proxy_user_password = $proxy_user_password;
+    }
+
+    public function makeReqOpt()
+    {
+        $reqOpt = new RequestOptions();
+        if ($this->proxy !== null) {
+            $reqOpt->proxy = $this->proxy;
+        }
+        if ($this->proxy_auth !== null) {
+            $reqOpt->proxy_auth = $this->proxy_auth;
+        }
+        if ($this->proxy_user_password !== null) {
+            $reqOpt->proxy_user_password = $this->proxy_user_password;
+        }
+        return $reqOpt;
+    }
+}

+ 25 - 1
sdk/Qiniu/Http/Request.php

@@ -3,16 +3,40 @@ namespace Qiniu\Http;
 
 final class Request
 {
+    /**
+     * @var string
+     */
     public $url;
+
+    /**
+     * @var array<string, string>
+     */
     public $headers;
+
+    /**
+     * @var mixed|null
+     */
     public $body;
+
+    /**
+     * @var string
+     */
     public $method;
 
-    public function __construct($method, $url, array $headers = array(), $body = null)
+    /**
+     * @var RequestOptions
+     */
+    public $opt;
+
+    public function __construct($method, $url, array $headers = array(), $body = null, $opt = null)
     {
         $this->method = strtoupper($method);
         $this->url = $url;
         $this->headers = $headers;
         $this->body = $body;
+        if ($opt === null) {
+            $opt = new RequestOptions();
+        }
+        $this->opt = $opt;
     }
 }

+ 104 - 0
sdk/Qiniu/Http/RequestOptions.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace Qiniu\Http;
+
+use Qiniu\Http\Middleware\Middleware;
+
+final class RequestOptions
+{
+
+    /**
+     * @var int|null
+     * http 请求的超时时间,单位:秒,默认:0,不超时
+     */
+    public $connection_timeout;
+
+    /**
+     * @var int|null
+     * http 请求的超时时间,单位:毫秒,默认:0,不超时
+     */
+    public $connection_timeout_ms;
+
+    /**
+     * @var int|null
+     * http 请求的超时时间,单位:秒,默认:0,不超时
+     */
+    public $timeout;
+
+
+    /**
+     * @var int|null
+     * http 请求的超时时间,单位:毫秒,默认:0,不超时
+     */
+    public $timeout_ms;
+
+    /**
+     * @var string|null
+     * 代理URL,默认:空
+     */
+    public $proxy;
+
+    /**
+     * @var int|null
+     * 代理鉴权方式,默认:空
+     */
+    public $proxy_auth;
+
+    /**
+     * @var string|null
+     * 代理鉴权参数,默认:空
+     */
+    public $proxy_user_password;
+
+    /**
+     * @var array<Middleware>
+     */
+    public $middlewares;
+
+    public function __construct(
+        $connection_timeout = null,
+        $connection_timeout_ms = null,
+        $timeout = null,
+        $timeout_ms = null,
+        $middlewares = array(),
+        $proxy = null,
+        $proxy_auth = null,
+        $proxy_user_password = null
+    ) {
+        $this->connection_timeout = $connection_timeout;
+        $this->connection_timeout_ms = $connection_timeout_ms;
+        $this->timeout = $timeout;
+        $this->timeout_ms = $timeout_ms;
+        $this->proxy = $proxy;
+        $this->proxy_auth = $proxy_auth;
+        $this->proxy_user_password = $proxy_user_password;
+        $this->middlewares = $middlewares;
+    }
+
+    public function getCurlOpt()
+    {
+        $result = array();
+        if ($this->connection_timeout != null) {
+            $result[CURLOPT_CONNECTTIMEOUT] = $this->connection_timeout;
+        }
+        if ($this->connection_timeout_ms != null) {
+            $result[CURLOPT_CONNECTTIMEOUT_MS] = $this->connection_timeout_ms;
+        }
+        if ($this->timeout != null) {
+            $result[CURLOPT_TIMEOUT] = $this->timeout;
+        }
+        if ($this->timeout_ms != null) {
+            $result[CURLOPT_TIMEOUT_MS] = $this->timeout_ms;
+        }
+        if ($this->proxy != null) {
+            $result[CURLOPT_PROXY] = $this->proxy;
+        }
+        if ($this->proxy_auth != null) {
+            $result[CURLOPT_PROXYAUTH] = $this->proxy_auth;
+        }
+        if ($this->proxy_user_password != null) {
+            $result[CURLOPT_PROXYUSERPWD] = $this->proxy_user_password;
+        }
+        return $result;
+    }
+}

+ 56 - 12
sdk/Qiniu/Http/Response.php

@@ -8,7 +8,21 @@ namespace Qiniu\Http;
 final class Response
 {
     public $statusCode;
+    /**
+     * deprecated because of field names case-sensitive.
+     * use $normalizedHeaders instead which field names are case-insensitive.
+     * but be careful not to use $normalizedHeaders with `array_*` functions,
+     * such as `array_key_exists`, `array_keys`, `array_values`.
+     *
+     * use `isset` instead of `array_key_exists`,
+     * and should never use `array_key_exists` at http header.
+     *
+     * use `foreach` instead of `array_keys`, `array_values`.
+     *
+     * @deprecated
+     */
     public $headers;
+    public $normalizedHeaders;
     public $body;
     public $error;
     private $jsonData;
@@ -87,21 +101,31 @@ final class Response
     {
         $this->statusCode = $code;
         $this->duration = $duration;
-        $this->headers = $headers;
+        $this->headers = array();
         $this->body = $body;
         $this->error = $error;
         $this->jsonData = null;
+
         if ($error !== null) {
             return;
         }
 
+        foreach ($headers as $k => $vs) {
+            if (is_array($vs)) {
+                $this->headers[$k] = $vs[count($vs) - 1];
+            } else {
+                $this->headers[$k] = $vs;
+            }
+        }
+        $this->normalizedHeaders = new Header($headers);
+
         if ($body === null) {
             if ($code >= 400) {
                 $this->error = self::$statusTexts[$code];
             }
             return;
         }
-        if (self::isJson($headers)) {
+        if (self::isJson($this->normalizedHeaders)) {
             try {
                 $jsonData = self::bodyJson($body);
                 if ($code >= 400) {
@@ -128,6 +152,19 @@ final class Response
         return $this->jsonData;
     }
 
+    public function headers($normalized = false)
+    {
+        if ($normalized) {
+            return $this->normalizedHeaders;
+        }
+        return $this->headers;
+    }
+
+    public function body()
+    {
+        return $this->body;
+    }
+
     private static function bodyJson($body)
     {
         return \Qiniu\json_decode((string) $body, true, 512);
@@ -135,24 +172,24 @@ final class Response
 
     public function xVia()
     {
-        $via = $this->headers['X-Via'];
+        $via = $this->normalizedHeaders['X-Via'];
         if ($via === null) {
-            $via = $this->headers['X-Px'];
+            $via = $this->normalizedHeaders['X-Px'];
         }
         if ($via === null) {
-            $via = $this->headers['Fw-Via'];
+            $via = $this->normalizedHeaders['Fw-Via'];
         }
         return $via;
     }
 
     public function xLog()
     {
-        return $this->headers['X-Log'];
+        return $this->normalizedHeaders['X-Log'];
     }
 
     public function xReqId()
     {
-        return $this->headers['X-Reqid'];
+        return $this->normalizedHeaders['X-Reqid'];
     }
 
     public function ok()
@@ -162,15 +199,22 @@ final class Response
 
     public function needRetry()
     {
-        $code = $this->statusCode;
-        if ($code < 0 || ($code / 100 === 5 and $code !== 579) || $code === 996) {
-            return true;
+        if ($this->statusCode > 0 && $this->statusCode < 500) {
+            return false;
+        }
+
+        // https://developer.qiniu.com/fusion/kb/1352/the-http-request-return-a-status-code
+        if (in_array($this->statusCode, array(
+            501, 509, 573, 579, 608, 612, 614, 616, 618, 630, 631, 632, 640, 701
+        ))) {
+            return false;
         }
+
+        return true;
     }
 
     private static function isJson($headers)
     {
-        return array_key_exists('Content-Type', $headers) &&
-        strpos($headers['Content-Type'], 'application/json') === 0;
+        return isset($headers['Content-Type']) && strpos($headers['Content-Type'], 'application/json') === 0;
     }
 }

+ 43 - 33
sdk/Qiniu/Processing/ImageUrlBuilder.php

@@ -1,4 +1,5 @@
 <?php
+
 namespace Qiniu\Processing;
 
 use Qiniu;
@@ -29,20 +30,29 @@ final class ImageUrlBuilder
      *
      * @var array
      */
-    protected $gravityArr = array('NorthWest', 'North', 'NorthEast',
-        'West', 'Center', 'East', 'SouthWest', 'South', 'SouthEast');
+    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 忽略结果
+     * @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>
@@ -105,15 +115,15 @@ final class ImageUrlBuilder
     /**
      * 图片水印
      *
-     * @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
+     * @param string $url 图片链接
+     * @param string $image 水印图片链接
+     * @param int $dissolve 透明度
+     * @param string $gravity 水印位置
+     * @param int $dx 横轴边距
+     * @param int $dy 纵轴边距
+     * @param int $watermarkScale 自适应原图的短边比例
      * @return string
+     * @link   http://developer.qiniu.com/code/v6/api/kodo-api/image/watermark.html
      * @author Sherlock Ren <sherlock_ren@icloud.com>
      */
     public function waterImg(
@@ -174,17 +184,17 @@ final class ImageUrlBuilder
     /**
      * 文字水印
      *
-     * @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
+     * @param string $url 图片链接
+     * @param string $text 文字
+     * @param string $font 文字字体
+     * @param string $fontSize 文字字号
+     * @param string $fontColor 文字颜色
+     * @param int $dissolve 透明度
+     * @param string $gravity 水印位置
+     * @param int $dx 横轴边距
+     * @param int $dy 纵轴边距
      * @return string
+     * @link   http://developer.qiniu.com/code/v6/api/kodo-api/image/watermark.html#text-watermark
      * @author Sherlock Ren <sherlock_ren@icloud.com>
      */
     public function waterText(
@@ -252,7 +262,7 @@ final class ImageUrlBuilder
     /**
      * 效验url合法性
      *
-     * @param  string $url url链接
+     * @param string $url url链接
      * @return string
      * @author Sherlock Ren <sherlock_ren@icloud.com>
      */
@@ -261,15 +271,15 @@ final class ImageUrlBuilder
         $urlArr = parse_url($url);
 
         return $urlArr['scheme']
-        && in_array($urlArr['scheme'], array('http', 'https'))
-        && $urlArr['host']
-        && $urlArr['path'];
+            && in_array($urlArr['scheme'], array('http', 'https'))
+            && $urlArr['host']
+            && $urlArr['path'];
     }
 
     /**
      * 检测是否有query
      *
-     * @param  string $url url链接
+     * @param string $url url链接
      * @return string
      * @author Sherlock Ren <sherlock_ren@icloud.com>
      */

+ 14 - 5
sdk/Qiniu/Processing/Operation.php

@@ -4,6 +4,7 @@ namespace Qiniu\Processing;
 
 use Qiniu\Http\Client;
 use Qiniu\Http\Error;
+use Qiniu\Http\Proxy;
 
 final class Operation
 {
@@ -11,20 +12,28 @@ final class Operation
     private $auth;
     private $token_expire;
     private $domain;
+    private $proxy;
 
-    public function __construct($domain, $auth = null, $token_expire = 3600)
-    {
+    public function __construct(
+        $domain,
+        $auth = null,
+        $token_expire = 3600,
+        $proxy = null,
+        $proxy_auth = null,
+        $proxy_user_password = null
+    ) {
         $this->auth = $auth;
         $this->domain = $domain;
         $this->token_expire = $token_expire;
+        $this->proxy = new Proxy($proxy, $proxy_auth, $proxy_user_password);
     }
 
 
     /**
      * 对资源文件进行处理
      *
-     * @param $key   待处理的资源文件名
-     * @param $fops   string|array  fop操作,多次fop操作以array的形式传入。
+     * @param string $key 待处理的资源文件名
+     * @param string $fops string|array  fop操作,多次fop操作以array的形式传入。
      *                eg. imageView2/1/w/200/h/200, imageMogr2/thumbnail/!75px
      *
      * @return array 文件处理后的结果及错误。
@@ -34,7 +43,7 @@ final class Operation
     public function execute($key, $fops)
     {
         $url = $this->buildUrl($key, $fops);
-        $resp = Client::get($url);
+        $resp = Client::get($url, array(), $this->proxy->makeReqOpt());
         if (!$resp->ok()) {
             return array(null, new Error($url, $resp));
         }

+ 18 - 11
sdk/Qiniu/Processing/PersistentFop.php

@@ -1,10 +1,11 @@
 <?php
+
 namespace Qiniu\Processing;
 
 use Qiniu\Config;
-use Qiniu\Http\Client;
 use Qiniu\Http\Error;
-use Qiniu\Processing\Operation;
+use Qiniu\Http\Client;
+use Qiniu\Http\Proxy;
 
 /**
  * 持久化处理类,该类用于主动触发异步持久化操作.
@@ -23,8 +24,13 @@ final class PersistentFop
      * */
     private $config;
 
+    /**
+     * @var 代理信息
+     */
+    private $proxy;
+
 
-    public function __construct($auth, $config = null)
+    public function __construct($auth, $config = null, $proxy = null, $proxy_auth = null, $proxy_user_password = null)
     {
         $this->auth = $auth;
         if ($config == null) {
@@ -32,17 +38,18 @@ final class PersistentFop
         } else {
             $this->config = $config;
         }
+        $this->proxy = new Proxy($proxy, $proxy_auth, $proxy_user_password);
     }
 
     /**
      * 对资源文件进行异步持久化处理
-     * @param $bucket     资源所在空间
-     * @param $key        待处理的源文件
-     * @param $fops       string|array  待处理的pfop操作,多个pfop操作以array的形式传入。
+     * @param string $bucket 资源所在空间
+     * @param string $key 待处理的源文件
+     * @param string $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      是否强制执行一次新的指令
+     * @param string $pipeline 资源处理队列
+     * @param string $notify_url 处理结果通知地址
+     * @param bool $force 是否强制执行一次新的指令
      *
      *
      * @return array 返回持久化处理的persistentId, 和返回的错误。
@@ -68,7 +75,7 @@ final class PersistentFop
         $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);
+        $response = Client::post($url, $data, $headers, $this->proxy->makeReqOpt());
         if (!$response->ok()) {
             return array(null, new Error($url, $response));
         }
@@ -85,7 +92,7 @@ final class PersistentFop
             $scheme = "https://";
         }
         $url = $scheme . Config::API_HOST . "/status/get/prefop?id=$id";
-        $response = Client::get($url);
+        $response = Client::get($url, array(), $this->proxy->makeReqOpt());
         if (!$response->ok()) {
             return array(null, new Error($url, $response));
         }

+ 229 - 0
sdk/Qiniu/Region.php

@@ -0,0 +1,229 @@
+<?php
+
+namespace Qiniu;
+
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+use Qiniu\Http\Middleware\RetryDomainsMiddleware;
+use Qiniu\Http\RequestOptions;
+
+class Region
+{
+
+    //源站上传域名
+    public $srcUpHosts;
+    //CDN加速上传域名
+    public $cdnUpHosts;
+    //资源管理域名
+    public $rsHost;
+    //资源列举域名
+    public $rsfHost;
+    //资源处理域名
+    public $apiHost;
+    //IOVIP域名
+    public $iovipHost;
+    // TTL
+    public $ttl;
+
+    //构造一个Region对象
+    public function __construct(
+        $srcUpHosts = array(),
+        $cdnUpHosts = array(),
+        $rsHost = "rs-z0.qiniuapi.com",
+        $rsfHost = "rsf-z0.qiniuapi.com",
+        $apiHost = "api.qiniuapi.com",
+        $iovipHost = null,
+        $ttl = null
+    ) {
+
+        $this->srcUpHosts = $srcUpHosts;
+        $this->cdnUpHosts = $cdnUpHosts;
+        $this->rsHost = $rsHost;
+        $this->rsfHost = $rsfHost;
+        $this->apiHost = $apiHost;
+        $this->iovipHost = $iovipHost;
+        $this->ttl = $ttl;
+    }
+
+    //华东机房
+    public static function regionHuadong()
+    {
+        $regionHuadong = new Region(
+            array("up.qiniup.com"),
+            array('upload.qiniup.com'),
+            'rs-z0.qiniuapi.com',
+            'rsf-z0.qiniuapi.com',
+            'api.qiniuapi.com',
+            'iovip.qbox.me'
+        );
+        return $regionHuadong;
+    }
+
+    //华东机房内网上传
+    public static function qvmRegionHuadong()
+    {
+        $qvmRegionHuadong = new Region(
+            array("free-qvm-z0-xs.qiniup.com"),
+            'rs-z0.qiniuapi.com',
+            'rsf-z0.qiniuapi.com',
+            'api.qiniuapi.com',
+            'iovip.qbox.me'
+        );
+        return $qvmRegionHuadong;
+    }
+
+    //华北机房内网上传
+    public static function qvmRegionHuabei()
+    {
+        $qvmRegionHuabei = new Region(
+            array("free-qvm-z1-zz.qiniup.com"),
+            "rs-z1.qiniuapi.com",
+            "rsf-z1.qiniuapi.com",
+            "api-z1.qiniuapi.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.qiniuapi.com",
+            "rsf-z1.qiniuapi.com",
+            "api-z1.qiniuapi.com",
+            "iovip-z1.qbox.me"
+        );
+
+        return $regionHuabei;
+    }
+
+    //华南机房
+    public static function regionHuanan()
+    {
+        $regionHuanan = new Region(
+            array('up-z2.qiniup.com'),
+            array('upload-z2.qiniup.com'),
+            "rs-z2.qiniuapi.com",
+            "rsf-z2.qiniuapi.com",
+            "api-z2.qiniuapi.com",
+            "iovip-z2.qbox.me"
+        );
+        return $regionHuanan;
+    }
+
+    //华东2 机房
+    public static function regionHuadong2()
+    {
+        return new Region(
+            array('up-cn-east-2.qiniup.com'),
+            array('upload-cn-east-2.qiniup.com'),
+            "rs-cn-east-2.qiniuapi.com",
+            "rsf-cn-east-2.qiniuapi.com",
+            "api-cn-east-2.qiniuapi.com",
+            "iovip-cn-east-2.qiniuio.com"
+        );
+    }
+
+    //北美机房
+    public static function regionNorthAmerica()
+    {
+        //北美机房
+        $regionNorthAmerica = new Region(
+            array('up-na0.qiniup.com'),
+            array('upload-na0.qiniup.com'),
+            "rs-na0.qiniuapi.com",
+            "rsf-na0.qiniuapi.com",
+            "api-na0.qiniuapi.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.qiniuapi.com",
+            "rsf-as0.qiniuapi.com",
+            "api-as0.qiniuapi.com",
+            "iovip-as0.qbox.me"
+        );
+        return $regionSingapore;
+    }
+
+    /*
+     * GET /v4/query?ak=<ak>&bucket=<bucket>
+     * @param string $ak
+     * @param string $bucket
+     * @param string $ucHost|null
+     * @param array $backupUcHosts
+     * @param int $retryTimes
+     * @param RequestOptions|null $reqOpt
+     * @return Response
+     **/
+    public static function queryRegion(
+        $ak,
+        $bucket,
+        $ucHost = null,
+        $backupUcHosts = array(),
+        $retryTimes = 2,
+        $reqOpt = null
+    ) {
+        $region = new Region();
+        if (!$ucHost) {
+            $ucHost = "https://" . Config::QUERY_REGION_HOST;
+        }
+        $url = $ucHost . '/v4/query' . "?ak=$ak&bucket=$bucket";
+        if ($reqOpt == null) {
+            $reqOpt = new RequestOptions();
+        }
+        $reqOpt->middlewares = array(
+            new RetryDomainsMiddleware(
+                $backupUcHosts,
+                $retryTimes
+            )
+        );
+        $ret = Client::get($url, array(), $reqOpt);
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        if (!is_array($r["hosts"]) || count($r["hosts"]) == 0) {
+            return array(null, new Error($url, $ret));
+        }
+
+        // parse region;
+        $regionHost = $r["hosts"][0];
+        $region->cdnUpHosts = array_merge($region->cdnUpHosts, $regionHost['up']['domains']);
+        $region->srcUpHosts = array_merge($region->srcUpHosts, $regionHost['up']['domains']);
+
+        // set specific hosts
+        $region->iovipHost = $regionHost['io']['domains'][0];
+        if (isset($regionHost['rs']['domains']) && count($regionHost['rs']['domains']) > 0) {
+            $region->rsHost = $regionHost['rs']['domains'][0];
+        } else {
+            $region->rsHost = Config::RS_HOST;
+        }
+        if (isset($regionHost['rsf']['domains']) && count($regionHost['rsf']['domains']) > 0) {
+            $region->rsfHost = $regionHost['rsf']['domains'][0];
+        } else {
+            $region->rsfHost = Config::RSF_HOST;
+        }
+        if (isset($regionHost['api']['domains']) && count($regionHost['api']['domains']) > 0) {
+            $region->apiHost = $regionHost['api']['domains'][0];
+        } else {
+            $region->apiHost = Config::API_HOST;
+        }
+
+        // set ttl
+        $region->ttl = $regionHost['ttl'];
+
+        return $region;
+    }
+}

+ 236 - 0
sdk/Qiniu/Rtc/AppClient.php

@@ -0,0 +1,236 @@
+<?php
+
+namespace Qiniu\Rtc;
+
+use Qiniu\Auth;
+use Qiniu\Config;
+use Qiniu\Http\Error;
+use Qiniu\Http\Client;
+use Qiniu\Http\Proxy;
+
+class AppClient
+{
+    private $auth;
+    private $baseURL;
+    private $proxy;
+
+    public function __construct(Auth $auth, $proxy = null, $proxy_auth = null, $proxy_user_password = null)
+    {
+        $this->auth = $auth;
+        $this->baseURL = sprintf("%s/%s/apps", Config::RTCAPI_HOST, Config::RTCAPI_VERSION);
+        $this->proxy = new Proxy($proxy, $proxy_auth, $proxy_user_password);
+    }
+
+    /**
+     * 创建应用
+     *
+     * @param string $hub 绑定的直播 hub
+     * @param string $title app 的名称  注意,Title 不是唯一标识,重复 create 动作将生成多个 app
+     * @param int $maxUsers 连麦房间支持的最大在线人数
+     * @param bool $noAutoKickUser 禁止自动踢人(抢流),默认为 false
+     * @return array
+     * @link  https://doc.qnsdk.com/rtn/docs/server_overview#2_1
+     */
+    public function createApp($hub, $title, $maxUsers = null, $noAutoKickUser = null)
+    {
+        $params = array();
+        $params['hub'] = $hub;
+        $params['title'] = $title;
+        if (!empty($maxUsers)) {
+            $params['maxUsers'] = $maxUsers;
+        }
+        if ($noAutoKickUser !== null) {
+            $params['noAutoKickUser'] = $noAutoKickUser;
+        }
+        $body = json_encode($params);
+        return $this->post($this->baseURL, $body);
+    }
+
+    /**
+     * 更新一个应用的配置信息
+     *
+     * @param string $appId app 的唯一标识,创建的时候由系统生成
+     * @param string $hub app 的名称,可选
+     * @param string $title 绑定的直播 hub,可选,用于合流后 rtmp 推流
+     * @param int $maxUsers 连麦房间支持的最大在线人数,可选
+     * @param bool $noAutoKickUser 禁止自动踢人,可选
+     * @param null $mergePublishRtmp 连麦合流转推 RTMP 的配置,可选择。其详细配置可以参考文档
+     * @return array
+     * @link  https://doc.qnsdk.com/rtn/docs/server_overview#2_1
+     */
+    public function updateApp($appId, $hub, $title, $maxUsers = null, $noAutoKickUser = null, $mergePublishRtmp = null)
+    {
+        $url = $this->baseURL . '/' . $appId;
+        $params = array();
+        $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);
+        return $this->post($url, $body);
+    }
+
+    /**
+     * 获取应用信息
+     *
+     * @param string $appId
+     * @return array
+     * @link  https://doc.qnsdk.com/rtn/docs/server_overview#2_1
+     */
+    public function getApp($appId)
+    {
+        $url = $this->baseURL . '/' . $appId;
+        return $this->get($url);
+    }
+
+    /**
+     * 删除应用
+     *
+     * @param string $appId app 的唯一标识,创建的时候由系统生成
+     * @return array
+     * @link  https://doc.qnsdk.com/rtn/docs/server_overview#2_1
+     */
+    public function deleteApp($appId)
+    {
+        $url = $this->baseURL . '/' . $appId;
+        return $this->delete($url);
+    }
+
+    /**
+     * 获取房间内用户列表
+     *
+     * @param string $appId app 的唯一标识,创建的时候由系统生成
+     * @param string $roomName 操作所查询的连麦房间
+     * @return array
+     * @link https://doc.qnsdk.com/rtn/docs/server_overview#2_2
+     */
+    public function listUser($appId, $roomName)
+    {
+        $url = sprintf("%s/%s/rooms/%s/users", $this->baseURL, $appId, $roomName);
+        return $this->get($url);
+    }
+
+    /**
+     * 指定一个用户踢出房间
+     *
+     * @param string $appId app 的唯一标识,创建的时候由系统生成
+     * @param string $roomName 连麦房间
+     * @param string $userId 操作所剔除的用户
+     * @return mixed
+     * @link https://doc.qnsdk.com/rtn/docs/server_overview#2_2
+     */
+    public function kickUser($appId, $roomName, $userId)
+    {
+        $url = sprintf("%s/%s/rooms/%s/users/%s", $this->baseURL, $appId, $roomName, $userId);
+        return $this->delete($url);
+    }
+
+    /**
+     * 停止一个房间的合流转推
+     *
+     * @param string $appId
+     * @param string $roomName
+     * @return array
+     * @link https://doc.qnsdk.com/rtn/docs/server_overview#2_2
+     */
+    public function stopMerge($appId, $roomName)
+    {
+        $url = sprintf("%s/%s/rooms/%s/merge", $this->baseURL, $appId, $roomName);
+        return $this->delete($url);
+    }
+
+    /**
+     * 获取应用中活跃房间
+     *
+     * @param string $appId 连麦房间所属的 app
+     * @param null $prefix 所查询房间名的前缀索引,可以为空。
+     * @param int $offset 分页查询的位移标记
+     * @param int $limit 此次查询的最大长度
+     * @return array
+     * @link https://doc.qnsdk.com/rtn/docs/server_overview#2_2
+     */
+    public function listActiveRooms($appId, $prefix = null, $offset = null, $limit = null)
+    {
+        $query = array();
+        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);
+        }
+        return $this->get($url);
+    }
+
+    /**
+     * 生成加入房间的令牌
+     *
+     * @param string $appId app 的唯一标识,创建的时候由系统生成
+     * @param string $roomName 房间名称,需满足规格 ^[a-zA-Z0-9_-]{3,64}$
+     * @param string $userId 请求加入房间的用户 ID,需满足规格 ^[a-zA-Z0-9_-]{3,50}$
+     * @param int $expireAt 鉴权的有效时间,传入以秒为单位的64位 Unix 绝对时间
+     * @param string $permission 该用户的房间管理权限,"admin" 或 "user",默认为 "user"
+     * @return string
+     * @link https://doc.qnsdk.com/rtn/docs/server_overview#1
+     */
+    public function appToken($appId, $roomName, $userId, $expireAt, $permission)
+    {
+        $params = array();
+        $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, $this->proxy->makeReqOpt());
+        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, $this->proxy->makeReqOpt());
+        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, $this->proxy->makeReqOpt());
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+}

+ 382 - 0
sdk/Qiniu/Sms/Sms.php

@@ -0,0 +1,382 @@
+<?php
+
+namespace Qiniu\Sms;
+
+use Qiniu\Auth;
+use Qiniu\Config;
+use Qiniu\Http\Error;
+use Qiniu\Http\Client;
+use Qiniu\Http\Proxy;
+
+class Sms
+{
+    private $auth;
+    private $baseURL;
+    private $proxy;
+
+    public function __construct(Auth $auth, $proxy = null, $proxy_auth = null, $proxy_user_password = null)
+    {
+        $this->auth = $auth;
+        $this->baseURL = sprintf("%s/%s/", Config::SMS_HOST, Config::SMS_VERSION);
+        $this->proxy = new Proxy($proxy, $proxy_auth, $proxy_user_password);
+    }
+
+    /**
+     * 创建签名
+     *
+     * @param string $signature 签名
+     * @param string $source 签名来源,申请签名时必须指定签名来源
+     * @param string $pics 签名对应的资质证明图片进行 base64 编码格式转换后的字符串,可选
+     * @return array
+     *
+     * @link https://developer.qiniu.com/sms/api/5844/sms-api-create-signature
+     */
+    public function createSignature($signature, $source, $pics = null)
+    {
+        $params = array();
+        $params['signature'] = $signature;
+        $params['source'] = $source;
+        if (!empty($pics)) {
+            $params['pics'] = array($this->imgToBase64($pics));
+        }
+        $body = json_encode($params);
+        $url = $this->baseURL . 'signature';
+        return $this->post($url, $body);
+    }
+
+    /**
+     * 编辑签名
+     *
+     * @param string $id 签名 ID
+     * @param string $signature 签名
+     * @param string $source 签名来源
+     * @param string $pics 签名对应的资质证明图片进行 base64 编码格式转换后的字符串,可选
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5890/sms-api-edit-signature
+     */
+    public function updateSignature($id, $signature, $source, $pics = null)
+    {
+        $params = array();
+        $params['signature'] = $signature;
+        $params['source'] = $source;
+        if (!empty($pics)) {
+            $params['pics'] = array($this->imgToBase64($pics));
+        }
+        $body = json_encode($params);
+        $url = $this->baseURL . 'signature/' . $id;
+        return $this->PUT($url, $body);
+    }
+
+    /**
+     * 列出签名
+     *
+     * @param string $audit_status 审核状态:"passed"(通过), "rejected"(未通过), "reviewing"(审核中)
+     * @param int $page 页码。默认为 1
+     * @param int $page_size 分页大小。默认为 20
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5889/sms-api-query-signature
+     */
+    public function querySignature($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
+        );
+        return $this->get($url);
+    }
+
+    /**
+     * 查询单个签名
+     *
+     * @param string $signature_id
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5970/query-a-single-signature
+     */
+    public function checkSingleSignature($signature_id)
+    {
+
+        $url = sprintf(
+            "%s/%s",
+            $this->baseURL . 'signature',
+            $signature_id
+        );
+        return $this->get($url);
+    }
+
+    /**
+     * 删除签名
+     *
+     * @param string $signature_id 签名 ID
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5891/sms-api-delete-signature
+     */
+    public function deleteSignature($signature_id)
+    {
+        $url = $this->baseURL . 'signature/' . $signature_id;
+        return $this->delete($url);
+    }
+
+    /**
+     * 创建模板
+     *
+     * @param string $name 模板名称
+     * @param string $template 模板内容 可设置自定义变量,发送短信时候使用,参考:${code}
+     * @param string $type notification:通知类,verification:验证码,marketing:营销类,voice:语音类
+     * @param string $description 申请理由简述
+     * @param string $signature_id 已经审核通过的签名
+     * @return array array
+     * @link https://developer.qiniu.com/sms/api/5893/sms-api-create-template
+     */
+    public function createTemplate(
+        $name,
+        $template,
+        $type,
+        $description,
+        $signature_id
+    ) {
+        $params = array();
+        $params['name'] = $name;
+        $params['template'] = $template;
+        $params['type'] = $type;
+        $params['description'] = $description;
+        $params['signature_id'] = $signature_id;
+
+        $body = json_encode($params);
+        $url = $this->baseURL . 'template';
+        return $this->post($url, $body);
+    }
+
+    /**
+     * 列出模板
+     *
+     * @param string $audit_status 审核状态:passed (通过), rejected (未通过), reviewing (审核中)
+     * @param int $page 页码。默认为 1
+     * @param int $page_size 分页大小。默认为 20
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5894/sms-api-query-template
+     */
+    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
+        );
+        return $this->get($url);
+    }
+
+    /**
+     * 查询单个模版
+     *
+     * @param string $template_id 模版ID
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5969/query-a-single-template
+     */
+    public function querySingleTemplate($template_id)
+    {
+
+        $url = sprintf(
+            "%s/%s",
+            $this->baseURL . 'template',
+            $template_id
+        );
+        return $this->get($url);
+    }
+
+    /**
+     * 编辑模板
+     *
+     * @param string $id 模板 ID
+     * @param string $name 模板名称
+     * @param string $template 模板内容
+     * @param string $description 申请理由简述
+     * @param string $signature_id 已经审核通过的签名 ID
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5895/sms-api-edit-template
+     */
+    public function updateTemplate(
+        $id,
+        $name,
+        $template,
+        $description,
+        $signature_id
+    ) {
+        $params = array();
+        $params['name'] = $name;
+        $params['template'] = $template;
+        $params['description'] = $description;
+        $params['signature_id'] = $signature_id;
+        $body = json_encode($params);
+        $url = $this->baseURL . 'template/' . $id;
+        return $this->PUT($url, $body);
+    }
+
+    /**
+     * 删除模板
+     *
+     * @param string $template_id 模板 ID
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5896/sms-api-delete-template
+     */
+    public function deleteTemplate($template_id)
+    {
+        $url = $this->baseURL . 'template/' . $template_id;
+        return $this->delete($url);
+    }
+
+    /**
+     * 发送短信
+     *
+     * @param string $template_id 模板 ID
+     * @param array $mobiles 手机号
+     * @param array $parameters 自定义模板变量,变量设置在创建模板时,参数template指定
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5897/sms-api-send-message
+     */
+    public function sendMessage($template_id, $mobiles, $parameters = null)
+    {
+        $params = array();
+        $params['template_id'] = $template_id;
+        $params['mobiles'] = $mobiles;
+        if (!empty($parameters)) {
+            $params['parameters'] = $parameters;
+        }
+        $body = json_encode($params);
+        $url = $this->baseURL . 'message';
+        return $this->post($url, $body);
+    }
+
+    /**
+     * 查询发送记录
+     *
+     * @param string $job_id 发送任务返回的 id
+     * @param string $message_id 单条短信发送接口返回的 id
+     * @param string $mobile 接收短信的手机号码
+     * @param string $status sending: 发送中,success: 发送成功,failed: 发送失败,waiting: 等待发送
+     * @param string $template_id 模版 id
+     * @param string $type marketing:营销,notification:通知,verification:验证码,voice:语音
+     * @param string $start 开始时间,timestamp,例如: 1563280448
+     * @param int $end 结束时间,timestamp,例如: 1563280471
+     * @param int $page 页码,默认为 1
+     * @param int $page_size 每页返回的数据条数,默认20,最大200
+     * @return array
+     * @link https://developer.qiniu.com/sms/api/5852/query-send-sms
+     */
+    public function querySendSms(
+        $job_id = null,
+        $message_id = null,
+        $mobile = null,
+        $status = null,
+        $template_id = null,
+        $type = null,
+        $start = null,
+        $end = null,
+        $page = 1,
+        $page_size = 20
+    ) {
+        $query = array();
+        \Qiniu\setWithoutEmpty($query, 'job_id', $job_id);
+        \Qiniu\setWithoutEmpty($query, 'message_id', $message_id);
+        \Qiniu\setWithoutEmpty($query, 'mobile', $mobile);
+        \Qiniu\setWithoutEmpty($query, 'status', $status);
+        \Qiniu\setWithoutEmpty($query, 'template_id', $template_id);
+        \Qiniu\setWithoutEmpty($query, 'type', $type);
+        \Qiniu\setWithoutEmpty($query, 'start', $start);
+        \Qiniu\setWithoutEmpty($query, 'end', $end);
+        \Qiniu\setWithoutEmpty($query, 'page', $page);
+        \Qiniu\setWithoutEmpty($query, 'page_size', $page_size);
+
+        $url = $this->baseURL . 'messages?' . http_build_query($query);
+        return $this->get($url);
+    }
+
+
+    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 !");
+                }
+                $img_type = null;
+                $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, $contentType = 'application/x-www-form-urlencoded')
+    {
+        $headers = $this->auth->authorizationV2($url, "GET", null, $contentType);
+        $headers['Content-Type'] = $contentType;
+        $ret = Client::get($url, $headers, $this->proxy->makeReqOpt());
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        return array($ret->json(), null);
+    }
+
+    private function delete($url, $contentType = 'application/json')
+    {
+        $headers = $this->auth->authorizationV2($url, "DELETE", null, $contentType);
+        $headers['Content-Type'] = $contentType;
+        $ret = Client::delete($url, $headers, $this->proxy->makeReqOpt());
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        return array($ret->json(), null);
+    }
+
+    private function post($url, $body, $contentType = 'application/json')
+    {
+        $headers = $this->auth->authorizationV2($url, "POST", $body, $contentType);
+
+        $headers['Content-Type'] = $contentType;
+        $ret = Client::post($url, $body, $headers, $this->proxy->makeReqOpt());
+        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')
+    {
+        $headers = $this->auth->authorizationV2($url, "PUT", $body, $contentType);
+        $headers['Content-Type'] = $contentType;
+        $ret = Client::put($url, $body, $headers, $this->proxy->makeReqOpt());
+        if (!$ret->ok()) {
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        return array($r, null);
+    }
+}

+ 131 - 0
sdk/Qiniu/Storage/ArgusManager.php

@@ -0,0 +1,131 @@
+<?php
+
+namespace Qiniu\Storage;
+
+use Qiniu\Auth;
+use Qiniu\Config;
+use Qiniu\Zone;
+use Qiniu\Http\Client;
+use Qiniu\Http\Error;
+use Qiniu\Http\Proxy;
+
+/**
+ * 主要涉及了内容审核接口的实现,具体的接口规格可以参考
+ *
+ * @link https://developer.qiniu.com/censor/api/5620/video-censor
+ */
+final class ArgusManager
+{
+    private $auth;
+    private $config;
+    private $proxy;
+
+    public function __construct(
+        Auth $auth,
+        Config $config = null,
+        $proxy = null,
+        $proxy_auth = null,
+        $proxy_user_password = null
+    ) {
+        $this->auth = $auth;
+        if ($config == null) {
+            $this->config = new Config();
+        } else {
+            $this->config = $config;
+        }
+        $this->proxy = new Proxy($proxy, $proxy_auth, $proxy_user_password);
+    }
+
+    /**
+     * 视频审核
+     *
+     * @param string $body body信息
+     *
+     * @return array 成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  https://developer.qiniu.com/censor/api/5620/video-censor
+     */
+    public function censorVideo($body)
+    {
+        $path = '/v3/video/censor';
+
+        return $this->arPost($path, $body);
+    }
+
+
+    /**
+     * 图片审核
+     *
+     * @param string $body
+     *
+     * @return array 成功返回NULL,失败返回对象Qiniu\Http\Error
+     * @link  https://developer.qiniu.com/censor/api/5588/image-censor
+     */
+    public function censorImage($body)
+    {
+        $path = '/v3/image/censor';
+
+        return $this->arPost($path, $body);
+    }
+
+    /**
+     * 查询视频审核结果
+     *
+     * @param string $jobid 任务ID
+     * @return array
+     * @link  https://developer.qiniu.com/censor/api/5620/video-censor
+     */
+    public function censorStatus($jobid)
+    {
+        $scheme = "http://";
+
+        if ($this->config->useHTTPS === true) {
+            $scheme = "https://";
+        }
+        $url = $scheme . Config::ARGUS_HOST . "/v3/jobs/video/$jobid";
+        $response = $this->get($url);
+        if (!$response->ok()) {
+            print("statusCode: " . $response->statusCode);
+            return array(null, new Error($url, $response));
+        }
+        return array($response->json(), null);
+    }
+
+    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 get($url)
+    {
+        $headers = $this->auth->authorizationV2($url, 'GET');
+
+        return Client::get($url, $headers, $this->proxy->makeReqOpt());
+    }
+
+    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, $this->proxy->makeReqOpt());
+        if (!$ret->ok()) {
+            print("statusCode: " . $ret->statusCode);
+            return array(null, new Error($url, $ret));
+        }
+        $r = ($ret->body === null) ? array() : $ret->json();
+        if (strstr($url, "video")) {
+            $jobid = $r['job'];
+            return array($jobid, null);
+        }
+        return array($r, null);
+    }
+}

File diff suppressed because it is too large
+ 749 - 110
sdk/Qiniu/Storage/BucketManager.php


+ 50 - 24
sdk/Qiniu/Storage/FormUploader.php

@@ -1,8 +1,11 @@
 <?php
+
 namespace Qiniu\Storage;
 
-use Qiniu\Http\Client;
+use Qiniu\Config;
 use Qiniu\Http\Error;
+use Qiniu\Http\Client;
+use Qiniu\Http\RequestOptions;
 
 final class FormUploader
 {
@@ -10,13 +13,15 @@ final class FormUploader
     /**
      * 上传二进制流到七牛, 内部使用
      *
-     * @param $upToken    上传凭证
-     * @param $key        上传文件名
-     * @param $data       上传二进制流
-     * @param $config     上传配置
-     * @param $params     自定义变量,规格参考
-     *                    http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
-     * @param $mime       上传数据的mimeType
+     * @param string $upToken 上传凭证
+     * @param string $key 上传文件名
+     * @param string $data 上传二进制流
+     * @param Config $config 上传配置
+     * @param string $params 自定义变量,规格参考
+     *                    {@link https://developer.qiniu.com/kodo/manual/1235/vars#xvar}
+     * @param string $mime 上传数据的mimeType
+     * @param string $fname
+     * @param RequestOptions $reqOpt
      *
      * @return array    包含已上传文件的信息,类似:
      *                                              [
@@ -31,12 +36,14 @@ final class FormUploader
         $config,
         $params,
         $mime,
-        $fname
+        $fname,
+        $reqOpt = null
     ) {
-
+        if ($reqOpt == null) {
+            $reqOpt = new RequestOptions();
+        }
         $fields = array('token' => $upToken);
         if ($key === null) {
-            $fname='nullkey';
         } else {
             $fields['key'] = $key;
         }
@@ -55,9 +62,22 @@ final class FormUploader
             return array(null, $err);
         }
 
-        $upHost = $config->getUpHost($accessKey, $bucket);
+        list($upHost, $err) = $config->getUpHostV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
 
-        $response = Client::multipartPost($upHost, $fields, 'file', $fname, $data, $mime);
+
+        $response = Client::multipartPost(
+            $upHost,
+            $fields,
+            'file',
+            $fname,
+            $data,
+            $mime,
+            array(),
+            $reqOpt
+        );
         if (!$response->ok()) {
             return array(null, new Error($upHost, $response));
         }
@@ -67,13 +87,13 @@ final class FormUploader
     /**
      * 上传文件到七牛,内部使用
      *
-     * @param $upToken    上传凭证
-     * @param $key        上传文件名
-     * @param $filePath   上传文件的路径
-     * @param $config     上传配置
-     * @param $params     自定义变量,规格参考
-     *                    http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
-     * @param $mime       上传数据的mimeType
+     * @param string $upToken 上传凭证
+     * @param string $key 上传文件名
+     * @param string $filePath 上传文件的路径
+     * @param Config $config 上传配置
+     * @param string $params 自定义变量,规格参考
+     *                    https://developer.qiniu.com/kodo/manual/1235/vars#xvar
+     * @param string $mime 上传数据的mimeType
      *
      * @return array    包含已上传文件的信息,类似:
      *                                              [
@@ -87,9 +107,12 @@ final class FormUploader
         $filePath,
         $config,
         $params,
-        $mime
+        $mime,
+        $reqOpt = null
     ) {
-
+        if ($reqOpt == null) {
+            $reqOpt = new RequestOptions();
+        }
 
         $fields = array('token' => $upToken, 'file' => self::createFile($filePath, $mime));
         if ($key !== null) {
@@ -111,9 +134,12 @@ final class FormUploader
             return array(null, $err);
         }
 
-        $upHost = $config->getUpHost($accessKey, $bucket);
+        list($upHost, $err) = $config->getUpHostV2($accessKey, $bucket, $reqOpt);
+        if ($err != null) {
+            return array(null, $err);
+        }
 
-        $response = Client::post($upHost, $fields, $headers);
+        $response = Client::post($upHost, $fields, $headers, $reqOpt);
         if (!$response->ok()) {
             return array(null, new Error($upHost, $response));
         }

+ 428 - 17
sdk/Qiniu/Storage/ResumeUploader.php

@@ -1,9 +1,12 @@
 <?php
+
 namespace Qiniu\Storage;
 
 use Qiniu\Config;
 use Qiniu\Http\Client;
 use Qiniu\Http\Error;
+use Qiniu\Enum\SplitUploadVersion;
+use Qiniu\Http\RequestOptions;
 
 /**
  * 断点续上传类, 该类主要实现了断点续上传中的分块上传,
@@ -21,19 +24,34 @@ final class ResumeUploader
     private $params;
     private $mime;
     private $contexts;
+    private $finishedEtags;
     private $host;
+    private $bucket;
     private $currentUrl;
     private $config;
+    private $resumeRecordFile;
+    private $version;
+    private $partSize;
+    /**
+     * @var RequestOptions
+     */
+    private $reqOpt;
 
     /**
      * 上传二进制流到七牛
      *
-     * @param $upToken    上传凭证
-     * @param $key        上传文件名
-     * @param $inputStream 上传二进制流
-     * @param $size       上传流的大小
-     * @param $params     自定义变量
-     * @param $mime       上传数据的mimeType
+     * @param string $upToken 上传凭证
+     * @param string $key 上传文件名
+     * @param resource $inputStream 上传二进制流
+     * @param int $size 上传流的大小
+     * @param array<string, string> $params 自定义变量
+     * @param string $mime 上传数据的mimeType
+     * @param Config $config
+     * @param string $resumeRecordFile 断点续传的已上传的部分信息记录文件
+     * @param string $version 分片上传版本 目前支持v1/v2版本 默认v1
+     * @param int $partSize 分片上传v2字段 默认大小为4MB 分片大小范围为1 MB - 1 GB
+     * @param RequestOptions $reqOpt 分片上传v2字段 默认大小为4MB 分片大小范围为1 MB - 1 GB
+     * @throws \Exception
      *
      * @link http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
      */
@@ -44,7 +62,11 @@ final class ResumeUploader
         $size,
         $params,
         $mime,
-        $config
+        $config,
+        $resumeRecordFile = null,
+        $version = 'v1',
+        $partSize = config::BLOCK_SIZE,
+        $reqOpt = null
     ) {
 
         $this->upToken = $upToken;
@@ -54,14 +76,29 @@ final class ResumeUploader
         $this->params = $params;
         $this->mime = $mime;
         $this->contexts = array();
+        $this->finishedEtags = array("etags" => array(), "uploadId" => "", "expiredAt" => 0, "uploaded" => 0);
         $this->config = $config;
+        $this->resumeRecordFile = $resumeRecordFile ? $resumeRecordFile : null;
+        $this->partSize = $partSize ? $partSize : config::BLOCK_SIZE;
+
+        if ($reqOpt === null) {
+            $reqOpt = new RequestOptions();
+        }
+        $this->reqOpt = $reqOpt;
+
+        try {
+            $this->version = SplitUploadVersion::from($version ? $version : 'v1');
+        } catch (\Exception $e) {
+            throw new \Exception("only support v1/v2 now!", 0, $e);
+        }
 
         list($accessKey, $bucket, $err) = \Qiniu\explodeUpToken($upToken);
+        $this->bucket = $bucket;
         if ($err != null) {
             return array(null, $err);
         }
 
-        $upHost = $config->getUpHost($accessKey, $bucket);
+        list($upHost, $err) = $config->getUpHostV2($accessKey, $bucket, $reqOpt);
         if ($err != null) {
             throw new \Exception($err->message(), 1);
         }
@@ -70,18 +107,102 @@ final class ResumeUploader
 
     /**
      * 上传操作
+     * @param $fname string 文件名
+     *
+     * @throws \Exception
      */
     public function upload($fname)
     {
+        $blkputRets = null;
+        // get upload record from resumeRecordFile
+        if ($this->resumeRecordFile != null) {
+            if (file_exists($this->resumeRecordFile)) {
+                $stream = fopen($this->resumeRecordFile, 'r');
+                if ($stream) {
+                    $streamLen = filesize($this->resumeRecordFile);
+                    if ($streamLen > 0) {
+                        $contents = fread($stream, $streamLen);
+                        fclose($stream);
+                        if ($contents) {
+                            $blkputRets = json_decode($contents, true);
+                            if ($blkputRets === null) {
+                                error_log("resumeFile contents decode error");
+                            }
+                        } else {
+                            error_log("read resumeFile failed");
+                        }
+                    } else {
+                        error_log("resumeFile is empty");
+                    }
+                } else {
+                    error_log("resumeFile open failed");
+                }
+            } else {
+                error_log("resumeFile not exists");
+            }
+        }
+
+        if ($this->version == SplitUploadVersion::V1) {
+            return $this->uploadV1($fname, $blkputRets);
+        } elseif ($this->version == SplitUploadVersion::V2) {
+            return $this->uploadV2($fname, $blkputRets);
+        } else {
+            throw new \Exception("only support v1/v2 now!");
+        }
+    }
+
+    /**
+     * @param string $fname 文件名
+     * @param null|array $blkputRets
+     *
+     * @throws \Exception
+     */
+    private function uploadV1($fname, $blkputRets = null)
+    {
+        // 尝试恢复恢复已上传的数据
+        $isResumeUpload = $blkputRets !== null;
+        $this->contexts = array();
+
+        if ($blkputRets) {
+            if (isset($blkputRets['contexts']) && isset($blkputRets['uploaded']) &&
+                is_array($blkputRets['contexts']) && is_int($blkputRets['uploaded'])
+            ) {
+                $this->contexts = array_map(function ($ctx) {
+                    if (is_array($ctx)) {
+                        return $ctx;
+                    } else {
+                        // 兼容旧版本(旧版本没有存储 expireAt)
+                        return array(
+                            "ctx" => $ctx,
+                            "expiredAt" => 0,
+                        );
+                    }
+                }, $blkputRets['contexts']);
+            }
+        }
+
+        // 上传分片
         $uploaded = 0;
         while ($uploaded < $this->size) {
             $blockSize = $this->blockSize($uploaded);
+            $blockIndex = $uploaded / $this->partSize;
+            if (!is_int($blockIndex)) {
+                throw new \Exception("v1 part size changed");
+            }
+            // 如果已上传该分片且没有过期
+            if (isset($this->contexts[$blockIndex]) && $this->contexts[$blockIndex]["expiredAt"] >= time()) {
+                $uploaded += $blockSize;
+                fseek($this->inputStream, $blockSize, SEEK_CUR);
+                continue;
+            }
             $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();
@@ -91,22 +212,180 @@ final class ResumeUploader
                 if ($err != null) {
                     return array(null, $err);
                 }
-
-                $upHostBackup = $this->config->getUpBackupHost($accessKey, $bucket);
+                list($upHostBackup, $err) = $this->config->getUpBackupHostV2($accessKey, $bucket, $this->reqOpt);
+                if ($err != null) {
+                    return array(null, $err);
+                }
                 $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']);
+
+            // 如果可以在已上传取到说明是过期分片直接修改已上传信息,否则是新的片添加到已上传分片尾部
+            if (isset($this->contexts[$blockIndex])) {
+                $this->contexts[$blockIndex] = array(
+                    'ctx' => $ret['ctx'],
+                    'expiredAt' => $ret['expired_at'],
+                );
+            } else {
+                array_push($this->contexts, array(
+                    'ctx' => $ret['ctx'],
+                    'expiredAt' => $ret['expired_at'],
+                ));
+            }
             $uploaded += $blockSize;
+
+            // 记录断点
+            if ($this->resumeRecordFile !== null) {
+                $recordData = array(
+                    'contexts' => $this->contexts,
+                    'uploaded' => $uploaded
+                );
+                $recordData = json_encode($recordData);
+
+                if ($recordData) {
+                    $isWritten = file_put_contents($this->resumeRecordFile, $recordData);
+                    if ($isWritten === false) {
+                        error_log("write resumeRecordFile failed");
+                    }
+                } else {
+                    error_log('resumeRecordData encode failed');
+                }
+            }
         }
-        return $this->makeFile($fname);
+
+        // 完成上传
+        list($ret, $err) = $this->makeFile($fname);
+        if ($err !== null) {
+            $response = $err->getResponse();
+            if ($isResumeUpload && $response->statusCode === 701) {
+                fseek($this->inputStream, 0);
+                return $this->uploadV1($fname);
+            }
+        }
+        return array($ret, $err);
+    }
+
+    /**
+     * @param string $fname 文件名
+     * @param null|array $blkputRets
+     *
+     * @throws \Exception
+     */
+    private function uploadV2($fname, $blkputRets = null)
+    {
+        $uploaded = 0;
+        $partNumber = 1;
+        $encodedObjectName = $this->key ? \Qiniu\base64_urlSafeEncode($this->key) : '~';
+        $isResumeUpload = $blkputRets !== null;
+
+        // 初始化 upload id
+        $err = null;
+        if ($blkputRets) {
+            if (isset($blkputRets["etags"]) && isset($blkputRets["uploadId"]) &&
+                isset($blkputRets["expiredAt"]) && $blkputRets["expiredAt"] > time() &&
+                $blkputRets["uploaded"] > 0 && is_array($blkputRets["etags"]) &&
+                is_string($blkputRets["uploadId"]) && is_int($blkputRets["expiredAt"])
+            ) {
+                $this->finishedEtags['etags'] = $blkputRets["etags"];
+                $this->finishedEtags["uploadId"] = $blkputRets["uploadId"];
+                $this->finishedEtags["expiredAt"] = $blkputRets["expiredAt"];
+                $this->finishedEtags["uploaded"] = $blkputRets["uploaded"];
+                $uploaded = $blkputRets["uploaded"];
+                $partNumber = count($this->finishedEtags["etags"]) + 1;
+            } else {
+                $err = $this->makeInitReq($encodedObjectName);
+            }
+        } else {
+            $err = $this->makeInitReq($encodedObjectName);
+        }
+        if ($err != null) {
+            return array(null, $err);
+        }
+
+        // 上传分片
+        fseek($this->inputStream, $uploaded);
+        while ($uploaded < $this->size) {
+            $blockSize = $this->blockSize($uploaded);
+            $data = fread($this->inputStream, $blockSize);
+            if ($data === false) {
+                throw new \Exception("file read failed", 1);
+            }
+            $md5 = md5($data);
+            $response = $this->uploadPart(
+                $data,
+                $partNumber,
+                $this->finishedEtags["uploadId"],
+                $encodedObjectName,
+                $md5
+            );
+
+            $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);
+                }
+                list($upHostBackup, $err) = $this->config->getUpBackupHostV2($accessKey, $bucket, $this->reqOpt);
+                if ($err != null) {
+                    return array(null, $err);
+                }
+                $this->host = $upHostBackup;
+            }
+
+            if ($response->needRetry() || !isset($ret['md5']) || $md5 != $ret['md5']) {
+                $response = $this->uploadPart(
+                    $data,
+                    $partNumber,
+                    $this->finishedEtags["uploadId"],
+                    $encodedObjectName,
+                    $md5
+                );
+                $ret = $response->json();
+            }
+            if ($isResumeUpload && $response->statusCode === 612) {
+                return $this->uploadV2($fname);
+            }
+            if (!$response->ok() || !isset($ret['md5']) || $md5 != $ret['md5']) {
+                return array(null, new Error($this->currentUrl, $response));
+            }
+            $blockStatus = array('etag' => $ret['etag'], 'partNumber' => $partNumber);
+            array_push($this->finishedEtags['etags'], $blockStatus);
+            $partNumber += 1;
+
+            $uploaded += $blockSize;
+            $this->finishedEtags['uploaded'] = $uploaded;
+
+            if ($this->resumeRecordFile !== null) {
+                $recordData = json_encode($this->finishedEtags);
+                if ($recordData) {
+                    $isWritten = file_put_contents($this->resumeRecordFile, $recordData);
+                    if ($isWritten === false) {
+                        error_log("write resumeRecordFile failed");
+                    }
+                } else {
+                    error_log('resumeRecordData encode failed');
+                }
+            }
+        }
+
+        list($ret, $err) = $this->completeParts($fname, $this->finishedEtags['uploadId'], $encodedObjectName);
+        if ($err !== null) {
+            $response = $err->getResponse();
+            if ($isResumeUpload && $response->statusCode === 612) {
+                return $this->uploadV2($fname);
+            }
+        }
+        return array($ret, $err);
     }
 
     /**
@@ -137,15 +416,25 @@ final class ResumeUploader
 
     /**
      * 创建文件
+     *
+     * @param string $fname 文件名
+     * @return array{array | null, Error | null}
      */
     private function makeFile($fname)
     {
         $url = $this->fileUrl($fname);
-        $body = implode(',', $this->contexts);
+        $body = implode(',', array_map(function ($ctx) {
+            return $ctx['ctx'];
+        }, $this->contexts));
         $response = $this->post($url, $body);
         if ($response->needRetry()) {
             $response = $this->post($url, $body);
         }
+        if ($response->statusCode === 200 || $response->statusCode === 701) {
+            if ($this->resumeRecordFile !== null) {
+                @unlink($this->resumeRecordFile);
+            }
+        }
         if (!$response->ok()) {
             return array(null, new Error($this->currentUrl, $response));
         }
@@ -156,14 +445,136 @@ final class ResumeUploader
     {
         $this->currentUrl = $url;
         $headers = array('Authorization' => 'UpToken ' . $this->upToken);
-        return Client::post($url, $data, $headers);
+        return Client::post($url, $data, $headers, $this->reqOpt);
     }
 
     private function blockSize($uploaded)
     {
-        if ($this->size < $uploaded + Config::BLOCK_SIZE) {
+        if ($this->size < $uploaded + $this->partSize) {
             return $this->size - $uploaded;
         }
-        return Config::BLOCK_SIZE;
+        return $this->partSize;
+    }
+
+    private function makeInitReq($encodedObjectName)
+    {
+        list($ret, $err) = $this->initReq($encodedObjectName);
+
+        if ($ret == null) {
+            return $err;
+        }
+
+        $this->finishedEtags["uploadId"] = $ret['uploadId'];
+        $this->finishedEtags["expiredAt"] = $ret['expireAt'];
+        return $err;
+    }
+
+    /**
+     * 初始化上传任务
+     */
+    private function initReq($encodedObjectName)
+    {
+        $url = $this->host . '/buckets/' . $this->bucket . '/objects/' . $encodedObjectName . '/uploads';
+        $headers = array(
+            'Authorization' => 'UpToken ' . $this->upToken,
+            'Content-Type' => 'application/json'
+        );
+        $response = $this->postWithHeaders($url, null, $headers);
+        $ret = $response->json();
+        if ($response->ok() && $ret != null) {
+            return array($ret, null);
+        }
+
+        return array(null, new Error($url, $response));
+    }
+
+    /**
+     * 分块上传v2
+     */
+    private function uploadPart($block, $partNumber, $uploadId, $encodedObjectName, $md5)
+    {
+        $headers = array(
+            'Authorization' => 'UpToken ' . $this->upToken,
+            'Content-Type' => 'application/octet-stream',
+            'Content-MD5' => $md5
+        );
+        $url = $this->host . '/buckets/' . $this->bucket . '/objects/' . $encodedObjectName .
+            '/uploads/' . $uploadId . '/' . $partNumber;
+        $response = $this->put($url, $block, $headers);
+        if ($response->statusCode === 612) {
+            if ($this->resumeRecordFile !== null) {
+                @unlink($this->resumeRecordFile);
+            }
+        }
+        return $response;
+    }
+
+    /**
+     * 完成分片上传V2
+     *
+     * @param string $fname 文件名
+     * @param int $uploadId 由 {@see initReq} 获取
+     * @param string $encodedObjectName 经过编码的存储路径
+     * @return array{array | null, Error | null}
+     */
+    private function completeParts($fname, $uploadId, $encodedObjectName)
+    {
+        $headers = array(
+            'Authorization' => 'UpToken ' . $this->upToken,
+            'Content-Type' => 'application/json'
+        );
+        $etags = $this->finishedEtags['etags'];
+        $sortedEtags = \Qiniu\arraySort($etags, 'partNumber');
+        $metadata = array();
+        $customVars = array();
+        if ($this->params) {
+            foreach ($this->params as $k => $v) {
+                if (strpos($k, 'x:') === 0) {
+                    $customVars[$k] = $v;
+                } elseif (strpos($k, 'x-qn-meta-') === 0) {
+                    $metadata[$k] = $v;
+                }
+            }
+        }
+        if (empty($metadata)) {
+            $metadata = null;
+        }
+        if (empty($customVars)) {
+            $customVars = null;
+        }
+        $body = array(
+            'fname' => $fname,
+            'mimeType' => $this->mime,
+            'metadata' => $metadata,
+            'customVars' => $customVars,
+            'parts' => $sortedEtags
+        );
+        $jsonBody = json_encode($body);
+        $url = $this->host . '/buckets/' . $this->bucket . '/objects/' . $encodedObjectName . '/uploads/' . $uploadId;
+        $response = $this->postWithHeaders($url, $jsonBody, $headers);
+        if ($response->needRetry()) {
+            $response = $this->postWithHeaders($url, $jsonBody, $headers);
+        }
+        if ($response->statusCode === 200 || $response->statusCode === 612) {
+            if ($this->resumeRecordFile !== null) {
+                @unlink($this->resumeRecordFile);
+            }
+        }
+        if (!$response->ok()) {
+            return array(null, new Error($this->currentUrl, $response));
+        }
+        return array($response->json(), null);
+    }
+
+    private function put($url, $data, $headers)
+    {
+        $this->currentUrl = $url;
+        return Client::put($url, $data, $headers, $this->reqOpt);
+    }
+
+    private function postWithHeaders($url, $data, $headers)
+    {
+        $this->currentUrl = $url;
+        return Client::post($url, $data, $headers, $this->reqOpt);
     }
 }

+ 58 - 26
sdk/Qiniu/Storage/UploadManager.php

@@ -3,6 +3,7 @@ namespace Qiniu\Storage;
 
 use Qiniu\Config;
 use Qiniu\Http\HttpClient;
+use Qiniu\Http\RequestOptions;
 use Qiniu\Storage\ResumeUploader;
 use Qiniu\Storage\FormUploader;
 
@@ -14,26 +15,40 @@ use Qiniu\Storage\FormUploader;
 final class UploadManager
 {
     private $config;
+    /**
+     * @var RequestOptions
+     */
+    private $reqOpt;
 
-    public function __construct(Config $config = null)
+    /**
+     * @param Config|null $config
+     * @param RequestOptions|null $reqOpt
+     */
+    public function __construct(Config $config = null, RequestOptions $reqOpt = null)
     {
         if ($config === null) {
             $config = new Config();
         }
         $this->config = $config;
+
+        if ($reqOpt === null) {
+            $reqOpt = new RequestOptions();
+        }
+
+        $this->reqOpt = $reqOpt;
     }
 
     /**
      * 上传二进制流到七牛
      *
-     * @param $upToken    上传凭证
-     * @param $key        上传文件名
-     * @param $data       上传二进制流
-     * @param $params     自定义变量,规格参考
+     * @param string $upToken 上传凭证
+     * @param string $key 上传文件名
+     * @param string $data 上传二进制流
+     * @param array<string, string> $params 自定义变量,规格参考
      *                    http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
-     * @param $mime       上传数据的mimeType
-     * @param $checkCrc   是否校验crc32
-     *
+     * @param string $mime 上传数据的mimeType
+     * @param string $fname
+     * @param RequestOptions $reqOpt
      * @return array    包含已上传文件的信息,类似:
      *                                              [
      *                                                  "hash" => "<Hash string>",
@@ -46,9 +61,11 @@ final class UploadManager
         $data,
         $params = null,
         $mime = 'application/octet-stream',
-        $fname = null
+        $fname = "default_filename",
+        $reqOpt = null
     ) {
-    
+        $reqOpt = $reqOpt === null ? $this->reqOpt : $reqOpt;
+
         $params = self::trimParams($params);
         return FormUploader::put(
             $upToken,
@@ -57,7 +74,8 @@ final class UploadManager
             $this->config,
             $params,
             $mime,
-            $fname
+            $fname,
+            $reqOpt
         );
     }
 
@@ -65,19 +83,23 @@ final class UploadManager
     /**
      * 上传文件到七牛
      *
-     * @param $upToken    上传凭证
-     * @param $key        上传文件名
-     * @param $filePath   上传文件的路径
-     * @param $params     自定义变量,规格参考
-     *                    http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
-     * @param $mime       上传数据的mimeType
-     * @param $checkCrc   是否校验crc32
+     * @param string $upToken 上传凭证
+     * @param string $key 上传文件名
+     * @param string $filePath 上传文件的路径
+     * @param array<string, mixed> $params 定义变量,规格参考
+     *                                     http://developer.qiniu.com/docs/v6/api/overview/up/response/vars.html#xvar
+     * @param boolean $mime 上传数据的mimeType
+     * @param string $checkCrc 是否校验crc32
+     * @param string $resumeRecordFile 断点续传文件路径 默认为null
+     * @param string $version 分片上传版本 目前支持v1/v2版本 默认v1
+     * @param int $partSize 分片上传v2字段 默认大小为4MB 分片大小范围为1 MB - 1 GB
      *
-     * @return array    包含已上传文件的信息,类似:
+     * @return array<string, mixed> 包含已上传文件的信息,类似:
      *                                              [
      *                                                  "hash" => "<Hash string>",
      *                                                  "key" => "<Key string>"
      *                                              ]
+     * @throws \Exception
      */
     public function putFile(
         $upToken,
@@ -85,9 +107,14 @@ final class UploadManager
         $filePath,
         $params = null,
         $mime = 'application/octet-stream',
-        $checkCrc = false
+        $checkCrc = false,
+        $resumeRecordFile = null,
+        $version = 'v1',
+        $partSize = config::BLOCK_SIZE,
+        $reqOpt = null
     ) {
-    
+        $reqOpt = $reqOpt === null ? $this->reqOpt : $reqOpt;
+
         $file = fopen($filePath, 'rb');
         if ($file === false) {
             throw new \Exception("file can not open", 1);
@@ -108,8 +135,8 @@ final class UploadManager
                 $this->config,
                 $params,
                 $mime,
-                $checkCrc,
-                basename($filePath)
+                basename($filePath),
+                $reqOpt
             );
         }
 
@@ -120,7 +147,11 @@ final class UploadManager
             $size,
             $params,
             $mime,
-            $this->config
+            $this->config,
+            $resumeRecordFile,
+            $version,
+            $partSize,
+            $reqOpt
         );
         $ret = $up->upload(basename($filePath));
         fclose($file);
@@ -134,8 +165,9 @@ final class UploadManager
         }
         $ret = array();
         foreach ($params as $k => $v) {
-            $pos = strpos($k, 'x:');
-            if ($pos === 0 && !empty($v)) {
+            $pos1 = strpos($k, 'x:');
+            $pos2 = strpos($k, 'x-qn-meta-');
+            if (($pos1 === 0 || $pos2 === 0) && !empty($v)) {
                 $ret[$k] = $v;
             }
         }

+ 30 - 143
sdk/Qiniu/Zone.php

@@ -1,171 +1,58 @@
 <?php
 namespace Qiniu;
 
-use Qiniu\Http\Client;
-use Qiniu\Http\Error;
+use Qiniu\Region;
 
-final class Zone
+class Zone extends Region
 {
-
-    //源站上传域名
-    public $srcUpHosts;
-    //CDN加速上传域名
-    public $cdnUpHosts;
-    //资源管理域名
-    public $rsHost;
-    //资源列举域名
-    public $rsfHost;
-    //资源处理域名
-    public $apiHost;
-    //IOVIP域名
-    public $iovipHost;
-
-    //构造一个Zone对象
-    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 zonez0()
+    {
+        return parent::regionHuadong();
     }
 
-    //华东机房
-    public static function zone0()
+    public static function zonez1()
     {
-        $Zone_z0 = new Zone(
-            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.qiniu.com',
-            'rsf.qiniu.com',
-            'api.qiniu.com',
-            'iovip.qbox.me'
-        );
-        return $Zone_z0;
+        return parent::regionHuabei();
     }
 
-    //华北机房
-    public static function zone1()
+    public static function zonez2()
     {
-        $Zone_z1 = new Zone(
-            array('up-z1.qiniup.com'),
-            array('upload-z1.qiniup.com'),
-            "rs-z1.qiniu.com",
-            "rsf-z1.qiniu.com",
-            "api-z1.qiniu.com",
-            "iovip-z1.qbox.me"
-        );
+        return parent::regionHuanan();
+    }
 
-        return $Zone_z1;
+    public static function zoneCnEast2()
+    {
+        return parent::regionHuadong2();
     }
 
-    //华南机房
-    public static function zone2()
+    public static function zoneAs0()
     {
-        $Zone_z2 = new Zone(
-            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.qiniu.com",
-            "rsf-z2.qiniu.com",
-            "api-z2.qiniu.com",
-            "iovip-z2.qbox.me"
-        );
-        return $Zone_z2;
+        return parent::regionSingapore();
     }
 
-    //北美机房
     public static function zoneNa0()
     {
-        //北美机房
-        $Zone_na0 = new Zone(
-            array('up-na0.qiniup.com'),
-            array('upload-na0.qiniup.com'),
-            "rs-na0.qiniu.com",
-            "rsf-na0.qiniu.com",
-            "api-na0.qiniu.com",
-            "iovip-na0.qbox.me"
-        );
-        return $Zone_na0;
+        return parent::regionNorthAmerica();
     }
 
-    //新加坡机房
-    public static function zoneAs0()
+    public static function qvmZonez0()
     {
-        //新加坡机房
-        $Zone_as0 = new Zone(
-            array('up-as0.qiniup.com'),
-            array('upload-as0.qiniup.com'),
-            "rs-as0.qiniu.com",
-            "rsf-as0.qiniu.com",
-            "api-as0.qiniu.com",
-            "iovip-as0.qbox.me"
-        );
-        return $Zone_as0;
+        return parent::qvmRegionHuadong();
     }
 
-    /*
-     * GET /v2/query?ak=<ak>&&bucket=<bucket>
-     **/
-    public static function queryZone($ak, $bucket)
+    public static function qvmZonez1()
     {
-        $zone = new Zone();
-        $url = Config::UC_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();
-        //print_r($ret);
-        //parse zone;
-
-        $iovipHost = $r['io']['src']['main'][0];
-        $zone->iovipHost = $iovipHost;
-        $accMain = $r['up']['acc']['main'][0];
-        array_push($zone->cdnUpHosts, $accMain);
-        if (isset($r['up']['acc']['backup'])) {
-            foreach ($r['up']['acc']['backup'] as $key => $value) {
-                array_push($zone->cdnUpHosts, $value);
-            }
-        }
-        $srcMain = $r['up']['src']['main'][0];
-        array_push($zone->srcUpHosts, $srcMain);
-        if (isset($r['up']['src']['backup'])) {
-            foreach ($r['up']['src']['backup'] as $key => $value) {
-                array_push($zone->srcUpHosts, $value);
-            }
-        }
-
-        //set specific hosts
-        if (strstr($zone->iovipHost, "z1") !== false) {
-            $zone->rsHost = "rs-z1.qiniu.com";
-            $zone->rsfHost = "rsf-z1.qiniu.com";
-            $zone->apiHost = "api-z1.qiniu.com";
-        } elseif (strstr($zone->iovipHost, "z2") !== false) {
-            $zone->rsHost = "rs-z2.qiniu.com";
-            $zone->rsfHost = "rsf-z2.qiniu.com";
-            $zone->apiHost = "api-z2.qiniu.com";
-        } elseif (strstr($zone->iovipHost, "na0") !== false) {
-            $zone->rsHost = "rs-na0.qiniu.com";
-            $zone->rsfHost = "rsf-na0.qiniu.com";
-            $zone->apiHost = "api-na0.qiniu.com";
-        } elseif (strstr($zone->iovipHost, "as0") !== false) {
-            $zone->rsHost = "rs-as0.qiniu.com";
-            $zone->rsfHost = "rsf-as0.qiniu.com";
-            $zone->apiHost = "api-as0.qiniu.com";
-        } else {
-            $zone->rsHost = "rs.qiniu.com";
-            $zone->rsfHost = "rsf.qiniu.com";
-            $zone->apiHost = "api.qiniu.com";
-        }
+        return parent::qvmRegionHuabei();
+    }
 
-        return $zone;
+    public static function queryZone(
+        $ak,
+        $bucket,
+        $ucHost = null,
+        $backupUcHosts = array(),
+        $retryTimes = 2,
+        $reqOpt = null
+    ) {
+        return parent::queryRegion($ak, $bucket, $ucHost, $backupUcHosts, $retryTimes, $reqOpt);
     }
 }

+ 92 - 10
sdk/Qiniu/functions.php

@@ -24,7 +24,7 @@ if (!defined('QINIU_FUNCTIONS_VERSION')) {
     /**
      * 计算输入流的crc32检验码
      *
-     * @param $data 待计算校验码的字符串
+     * @param $data string 待计算校验码的字符串
      *
      * @return string 输入字符串的crc32校验码
      */
@@ -64,6 +64,23 @@ if (!defined('QINIU_FUNCTIONS_VERSION')) {
         return base64_decode(str_replace($find, $replace, $str));
     }
 
+    /**
+     * 二维数组根据某个字段排序
+     * @param array $array 要排序的数组
+     * @param string $key 要排序的键
+     * @param string $sort  排序类型 SORT_ASC SORT_DESC
+     * return array 排序后的数组
+     */
+    function arraySort($array, $key, $sort = SORT_ASC)
+    {
+        $keysValue = array();
+        foreach ($array as $k => $v) {
+            $keysValue[$k] = $v[$key];
+        }
+        array_multisort($keysValue, $sort, $array);
+        return $array;
+    }
+
     /**
      * Wrapper for JSON decode that implements error detection with helpful
      * error messages.
@@ -108,27 +125,37 @@ if (!defined('QINIU_FUNCTIONS_VERSION')) {
     /**
      * 计算七牛API中的数据格式
      *
-     * @param $bucket 待操作的空间名
-     * @param $key 待操作的文件名
+     * @param string $bucket 待操作的空间名
+     * @param string $key 待操作的文件名
      *
      * @return string  符合七牛API规格的数据格式
-     * @link http://developer.qiniu.com/docs/v6/api/reference/data-formats.html
+     * @link https://developer.qiniu.com/kodo/api/data-format
      */
-    function entry($bucket, $key)
+    function entry($bucket, $key = null)
     {
         $en = $bucket;
-        if (!empty($key)) {
+        if ($key !== null) {
             $en = $bucket . ':' . $key;
         }
         return base64_urlSafeEncode($en);
     }
 
+    function decodeEntry($entry)
+    {
+        $en = base64_urlSafeDecode($entry);
+        $en = explode(':', $en);
+        if (count($en) == 1) {
+            return array($en[0], null);
+        }
+        return array($en[0], $en[1]);
+    }
+
     /**
      * array 辅助方法,无值时不set
      *
-     * @param $array 待操作array
-     * @param $key key
-     * @param $value value 为null时 不设置
+     * @param array $array 待操作array
+     * @param string $key key
+     * @param string $value value 为null时 不设置
      *
      * @return array 原来的array,便于连续操作
      */
@@ -255,10 +282,65 @@ if (!defined('QINIU_FUNCTIONS_VERSION')) {
             return array(null, null, "invalid uptoken");
         }
         $accessKey = $items[0];
-        $putPolicy = json_decode(base64_decode($items[2]));
+        $putPolicy = json_decode(base64_urlSafeDecode($items[2]));
         $scope = $putPolicy->scope;
         $scopeItems = explode(':', $scope);
         $bucket = $scopeItems[0];
         return array($accessKey, $bucket, null);
     }
+
+    // polyfill ucwords for `php version < 5.4.32` or `5.5.0 <= php version < 5.5.16`
+    if (version_compare(phpversion(), "5.4.32") < 0 ||
+        (
+            version_compare(phpversion(), "5.5.0") >= 0 &&
+            version_compare(phpversion(), "5.5.16") < 0
+        )
+    ) {
+        function ucwords($str, $delimiters = " \t\r\n\f\v")
+        {
+            $delims = preg_split('//u', $delimiters, -1, PREG_SPLIT_NO_EMPTY);
+
+            foreach ($delims as $delim) {
+                $str = implode($delim, array_map('ucfirst', explode($delim, $str)));
+            }
+
+            return $str;
+        }
+    } else {
+        function ucwords($str, $delimiters)
+        {
+            return \ucwords($str, $delimiters);
+        }
+    }
+
+    /**
+     * 将 parse_url 的结果转换回字符串
+     * TODO: add unit test
+     *
+     * @param $parsed_url - parse_url 的结果
+     * @return string
+     */
+    function unparse_url($parsed_url)
+    {
+
+        $scheme   = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
+
+        $host     = isset($parsed_url['host']) ? $parsed_url['host'] : '';
+
+        $port     = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
+
+        $user     = isset($parsed_url['user']) ? $parsed_url['user'] : '';
+
+        $pass     = isset($parsed_url['pass']) ? ':' . $parsed_url['pass']  : '';
+
+        $pass     = ($user || $pass) ? "$pass@" : '';
+
+        $path     = isset($parsed_url['path']) ? $parsed_url['path'] : '';
+
+        $query    = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
+
+        $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
+
+        return "$scheme$user$pass$host$port$path$query$fragment";
+    }
 }

+ 12 - 22
sdk/qiniu.php

@@ -1,23 +1,13 @@
 <?php
-
-Dever::apply('Qiniu/Config', 'upload', 'sdk');
-Dever::apply('Qiniu/functions', 'upload', 'sdk');
-
-Dever::apply('Qiniu/Http/Client', 'upload', 'sdk');
-Dever::apply('Qiniu/Http/Error', 'upload', 'sdk');
-Dever::apply('Qiniu/Zone', 'upload', 'sdk');
-
-Dever::apply('Qiniu/Auth', 'upload', 'sdk');
-
-Dever::apply('Qiniu/Http/Request', 'upload', 'sdk');
-Dever::apply('Qiniu/Http/Response', 'upload', 'sdk');
-
-Dever::apply('Qiniu/Storage/ResumeUploader', 'upload', 'sdk');
-Dever::apply('Qiniu/Storage/FormUploader', 'upload', 'sdk');
-
-
-Dever::apply('Qiniu/Storage/BucketManager', 'upload', 'sdk');
-Dever::apply('Qiniu/Storage/UploadManager', 'upload', 'sdk');
-
-Dever::apply('Qiniu/Processing/Operation', 'upload', 'sdk');
-Dever::apply('Qiniu/Processing/PersistentFop', 'upload', 'sdk');
+function classLoader($class)
+{
+    $path = str_replace('\\', DIRECTORY_SEPARATOR, $class);
+    $file = __DIR__ . '/' . $path . '.php';
+
+    if (file_exists($file)) {
+        require_once $file;
+    }
+}
+spl_autoload_register('classLoader');
+require_once  __DIR__ . '/Qiniu/functions.php';
+require_once  __DIR__ . '/Qiniu/Http/Middleware/Middleware.php';

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