diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6c45869 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*.php] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 223e487..27e8277 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .DS_Store -vendor/ +.idea +vendor composer.lock -.idea/ diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..56ace39 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,9 @@ +filter: + paths: + - 'src/*' + - 'tests/*' + +tools: + php_code_sniffer: + config: + standard: PSR4 diff --git a/.travis.yml b/.travis.yml index 4a6004f..2ad08f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,6 @@ language: php php: - - "5.3" - - "5.4" - "5.5" - - "5.6" - - "7.0" before_script: - composer install script: diff --git a/LICENSE b/LICENSE index 344fcd7..b1c803e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012 UPYUN +Copyright (c) 2016 UPYUN Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 84975c1..b91c582 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,22 @@ # 又拍云PHP SDK ![build](https://travis-ci.org/upyun/php-sdk.svg) -又拍云存储PHP SDK,基于 [又拍云存储HTTP REST API接口](http://docs.upyun.com/api/rest_api/) 开发。 +又拍云存储PHP SDK,基于[又拍云存储 HTTP API 接口](http://docs.upyun.com/api/) 开发。SDK 包含了文件上传下载刷新等基本操作,以及图片、视频云处理等功能。 + - [更新说明](#update instructions) - [使用说明](#use instructions) - [安装](#install) - - [初始化UpYun](#init) -- [示例](#usage) - - [上传文件](#upload file) - - [上传图片](#upload img) - - [下载文件](#download file) - - [创建目录](#mkdir) - - [删除目录或者文件](#delete) - - [获取目录文件列表](#file list) - - [获取文件信息](#file info) - - [获取空间使用状况](#bucket info) -- [异常处理](#exception) + - [使用](#usage) - [贡献代码](#contribute) - [社区](#community) - [许可证](#license) ## 更新说明 +#### 3.0.0 + +- 重写 API 接口,不兼容 2.x 版本 +- 集合分块、刷新、视频预处理功能 #### 2.2.0 @@ -45,195 +40,44 @@ composer require upyun/sdk ``` - -### 初始化UpYun -```php -require_once('vendor/autoload.php'); -$upyun = new UpYun('bucketname', 'operator_name', 'operator_pwd'); -``` - -参数 `bucketname` 为空间名称,`operator_name`、`operator_pwd` 为授权操作员的账号密码。 - -根据国内的网络情况,又拍云存储API目前提供了电信、联通网通、移动铁通三个接入点,在初始化的时候可以添加可选的第四个参数来指定API接入点。 - -```php -$upyun = new UpYun('bucketname', 'operator_name', 'operator_pwd', UpYun::ED_TELECOM); -``` - -接入点有四个值可选: - -* `UpYun::ED_AUTO` 根据网络条件自动选择接入点 -* `UpYun::ED_TELECOM` 电信接入点 -* `UpYun::ED_CNC` 联通网通接入点 -* `UpYun::ED_CTT` 移动铁通接入点 - -默认参数为自动选择API接入点。但是我们推荐根据服务器网络状况,手动设置合理的接入点已获取最佳的访问速度。 - -**超时时间设置** - -在初始化UpYun上传时,可以选择设置上传请求超时时间(默认30s): -```php -$upyun = new UpYun('bucketname', 'operator_name', 'operator_pwd', UpYun::ED_TELECOM, 600); -``` - -## 示例 - -*示例代码中所有`bucketname`,`operator_name`,`operator_pwd`以及路径需要替换成实际环境的值,账户密码请注意保密* - - -### 上传文件 +### 初始化 -文件类空间可以上传任意形式的二进制文件 - -**1.直接读取整个文件内容:** ```php -$upyun->writeFile('/path/to/server/file.ext', 'your file content', true); -``` +require_once('vendor/autoload.php'); -**2.文件流的方式上传,可降低内存占用:** -```php -$file_handler = fopen('demo.png', 'r'); -$upyun->writeFile('/path/to/server/demo.png', $file_handler, true); -fclose($file_handler); +use Upyun\Upyun; +use Upyun\Config; +$bucketConfig = new Config('yourBucketName', 'yourOperatorName', 'yourOperatorPwd'); +$client = new Upyun($bucketConfig); ``` -`writeFile()`第三个参数为可选,`true`表示自动创建相应目录,默认值为`false`。 -文件空间上传成功后返回`true`。 -如果上传失败,则会抛出异常。 - - -### 上传图片 -图片可以上传到图片类空间或文件类空间 -* 图片空间上传的图片不能超过20M,图片`宽*高*帧数`不能超过`2亿` -* 文件空间上传的图片不能超过1G -*建议站点图片上传到图片空间,便于在请求图片时可以生成自定义版本图片* - -**1.上传图片并创建缩略图:** -`writeFile()`方法第四个参数为数组类型可选参数,用来设置文件类型、缩略图处理。 -```php -$opts = array( - UpYun::X_GMKERL_THUMBNAIL => 'square' //创建缩略图 -); +1. 字符串写入又拍云服务器 -$fh = fopen('demo.png', 'r'); -$upyun->writeFile('/temp/upload_demo.png', $fh, true, $opts); -fclose($fh); ``` -`writeFile()`方法第四个参数可以设置的值还包括: - -* UpYun::CONTENT_TYPE -* UpYun::CONTENT_MD5 -* UpYun::CONTENT_SECRET -* UpYun::X_GMKERL_THUMBNAIL -* UpYun::X_GMKERL_TYPE -* UpYun::X_GMKERL_VALUE -* UpYun::X_GMKERL_QUALITY -* UpYun::X_GMKERL_UNSHARP - -参数的具体使用方法,请参考[标准API上传文件](http://docs.upyun.com/api/rest_api/#_4) - -* 图片空间上传成功后会返回一维数组,包含了图片信息,示例如下: - -```php -array( - 'x-upyun-width' => 2000, - 'x-upyun-height' => 1000, - 'x-upyun-frames' => 1 - 'x-upyun-file-type' => "JPEG" -) +$client->write('/save/path', 'file content'); ``` -如果上传失败,则会抛出异常。 - -### 下载文件 +2. 文件流写入又拍云服务器 -**1.直接读取文件内容:** -```php -$data = $upyun->readFile('/temp/upload_demo.png'); ``` - -**2.使用文件流模式下载:** -```php -$fh = fopen('/tmp/demo.png', 'w'); -$upyun->readFile('/temp/upload_demo.png', $fh); -fclose($fh); +$file = fopen('/local/path/file', 'r'); +$client->write('/save/path', $file); ``` -直接获取文件时,返回文件内容,使用数据流形式获取时,成功返回`true`。 -如果获取文件失败,则抛出异常。 - - -### 创建目录 -```php -$upyun->makeDir('/demo/'); -``` -目录路径必须以斜杠 `/` 结尾,创建成功返回 `true`,否则抛出异常。 +3. 上传图片并转换格式为 `png`,详见[上传作图](http://docs.upyun.com/cloud/image/#_2) - -### 删除目录或者文件 -```php -$upyun->delete('/demo/'); // 删除目录 -$upyun->delete('/demo/demo.png'); // 删除文件 ``` -删除成功返回`true`,否则抛出异常。注意删除目录时,`必须保证目录为空` ,否则也会抛出异常。 - - -### 获取目录文件列表 -```php -$list = $upyun->getList('/demo/'); -$file = $list[0]; -echo $file['name']; // 文件名 -echo $file['type']; // 类型(目录: folder; 文件: file) -echo $file['size']; // 尺寸 -echo $file['time']; // 创建时间 +$file = fopen('/local/path/image.jpg', 'r'); +$client->write('/save/image.png', $file, array('x-gmkerl-thumb' => '/format/png')); ``` -获取目录文件以及子目录列表。需要获取根目录列表是,使用 `$upyun->getList('/')` ,或直接表用方法不传递参数。 -目录获取失败则抛出异常。 - -### 获取文件信息 -```php -$result = $upyun->getFileInfo('/demo/demo.png'); -echo $result['x-upyun-file-type']; // 文件类型 -echo $result['x-upyun-file-size']; // 文件大小 -echo $result['x-upyun-file-date']; // 创建日期 -``` -返回结果为一个数组。 +4. 下载文件并保存到本地 - -### 获取空间使用状况 -```php -$upyun->getBucketUsage(); // 获取Bucket空间使用情况 ``` -返回的结果为空间使用量,单位 ***Byte*** - - -## 异常处理 -当API请求发生错误时,SDK将抛出异常,具体错误代码请参考[标准API错误代码表](http://docs.upyun.com/api/rest_api/#rest-api) - -根据返回HTTP CODE的不同,SDK将抛出以下异常: - -* **UpYunAuthorizationException** 401,授权错误 -* **UpYunForbiddenException** 403,权限错误 -* **UpYunNotFoundException** 404,文件或目录不存在 -* **UpYunNotAcceptableException** 406, 目录错误 -* **UpYunServiceUnavailable** 503,系统错误 - -未包含在以上异常中的错误,将统一抛出 `UpYunException` 异常。 - -为了正确处理API请求中可能出现的异常,建议将API操作放在`try{...}catch(Exception -$e){…}` 块中,如下所示: - -```php -try { - $upyun->getFolderUsage('/demo/'); - //your code here - -} catch(Exception $e) { - echo $e->getCode(); // 错误代码 - echo $e->getMessage(); // 具体错误信息 -} +$saveLocal = fopen('/local/path/image.jpg', 'w'); +// 第二个参数不传时,read 方法将直接返回文件内容 +$client->read('/remote/server/image.png', $saveLocal); ``` @@ -245,13 +89,13 @@ try { ## 社区 - - [UPYUN问答社区](http://segmentfault.com/upyun) - - [UPYUN微博](http://weibo.com/upaiyun) + - [问答社区](http://segmentfault.com/upyun) + - [微博](http://weibo.com/upaiyun) ## 许可证 -UPYUN PHP-SDK基于 MIT 开源协议 +UPYUN PHP-SDK 基于 MIT 开源协议 diff --git a/composer.json b/composer.json index e1aa1b4..d18f438 100644 --- a/composer.json +++ b/composer.json @@ -7,11 +7,19 @@ "homepage": "https://github.com/upyun/php-sdk/", "license": "MIT", "require": { - "php": ">=5.3.0", - "ext-curl": "*" + "php": ">=5.5.0", + "ext-curl": "*", + "guzzlehttp/guzzle": "~6.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~4.0", + "phpdocumentor/phpdocumentor": "^2.9" + }, + "autoload": { + "psr-4": { "Upyun\\": "src/Upyun/" } + }, + "autoload-dev": { + "psr-4": { "Upyun\\Tests\\": "tests/" } }, "authors": [ { @@ -30,8 +38,5 @@ "name": "sabakugaara", "email": "senellise@gmail.com" } - ], - "autoload": { - "files": ["upyun.class.php"] - } + ] } diff --git a/examples/client-upload/Readme.md b/examples/client-upload/Readme.md new file mode 100644 index 0000000..ca48770 --- /dev/null +++ b/examples/client-upload/Readme.md @@ -0,0 +1,12 @@ +## 客户端上传 + +本示例展示了如何使用表单 API, 直接从客户端进行安全的文件上传, 这种方式不需要客户服务器进行中转, 节省了客户服务器流量, 并且支持 HTTP/HTTPS 两种协议 + +DEMO 使用 `sdkimg` 空间进行演示, 上传成功后, 访问路径为 `http://sdkimg.b0.upaiyun.com/` 拼接保存路径 + +#### 运行示例 + +- `cd examples/client-upload` +- `php -S localhost:9000` + +打开浏览器访问 `http://localhost:9000`, 选则文件上传即可. \ No newline at end of file diff --git a/examples/client-upload/index.html b/examples/client-upload/index.html new file mode 100644 index 0000000..5fca3a2 --- /dev/null +++ b/examples/client-upload/index.html @@ -0,0 +1,46 @@ + + + + + Document + + + + + + +
+
+ Client Upload Demo + + + + +
+
+ + + + diff --git a/examples/client-upload/normalize.css b/examples/client-upload/normalize.css new file mode 100644 index 0000000..73454f7 --- /dev/null +++ b/examples/client-upload/normalize.css @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} \ No newline at end of file diff --git a/examples/client-upload/policy.php b/examples/client-upload/policy.php new file mode 100644 index 0000000..b86aaf5 --- /dev/null +++ b/examples/client-upload/policy.php @@ -0,0 +1,15 @@ +setFormApiKey('Mv83tlocuzkmfKKUFbz2s04FzTw='); + +$data['save-key'] = $_GET['save_path']; +$data['expiration'] = time() + 120; +$policy = Signature::getFormSignature($config, $data); +echo json_encode($policy); + + diff --git a/examples/get_list.php b/examples/get_list.php deleted file mode 100644 index f88c31d..0000000 --- a/examples/get_list.php +++ /dev/null @@ -1,20 +0,0 @@ - 'tester', - 'pwd' => 'grjxv2mxELR3', - 'bucket' => 'sdkimg', - 'picture_path' => dirname(__FILE__) . '/assets/sample.jpeg' -); -$upyun = new UpYun($config['bucket'], $config['user_name'], $config['pwd']); - -try { - echo "=========获取目录文件列表\r\n"; - $list = $upyun->getList('/demo/'); - var_dump($list); - echo "=========DONE\r\n\r\n"; -} -catch(Exception $e) { - echo $e->getCode(); - echo $e->getMessage(); -} diff --git a/examples/sample.jpeg b/examples/sample.jpeg deleted file mode 100644 index bbb067d..0000000 Binary files a/examples/sample.jpeg and /dev/null differ diff --git a/examples/write_file.php b/examples/write_file.php deleted file mode 100644 index 266fbfd..0000000 --- a/examples/write_file.php +++ /dev/null @@ -1,55 +0,0 @@ - 'tester', - 'pwd' => 'grjxv2mxELR3', - 'bucket' => 'sdkimg', - 'picture_path' => dirname(__FILE__) . '/assets/sample.jpeg' -); -$upyun = new UpYun($config['bucket'], $config['user_name'], $config['pwd']); - -try { - echo "=========直接上传文件\r\n"; - $fh = fopen(__DIR__.'/sample.jpeg', 'rb'); - $rsp = $upyun->writeFile('/demo/sample_normal.jpeg', $fh, True); // 上传图片,自动创建目录 - fclose($fh); - var_dump($rsp); - echo "=========DONE\n\r\n"; - - echo "=========设置MD5校验文件完整性\r\n"; - $opts = array( - UpYun::CONTENT_MD5 => md5(file_get_contents(__DIR__.'/sample.jpeg')) - ); - $fh = fopen(__DIR__.'/sample.jpeg', 'rb'); - $rsp = $upyun->writeFile('/demo/sample_md5.jpeg', $fh, True, $opts); // 上传图片,自动创建目录 - fclose($fh); - var_dump($rsp); - echo "=========DONE\r\n\r\n"; - - echo "=========直接生成缩略图,不保存原图片,仅对图片文件有效\r\n"; - $opts = array( - UpYun::X_GMKERL_TYPE => 'square', // 缩略图类型 - UpYun::X_GMKERL_VALUE => 150, // 缩略图大小 - UpYun::X_GMKERL_QUALITY => 95, // 缩略图压缩质量 - UpYun::X_GMKERL_UNSHARP => True // 是否进行锐化处理 - ); - $fh = fopen(__DIR__.'/sample.jpeg', 'rb'); - $rsp = $upyun->writeFile('/demo/sample_thumb_1.jpeg', $fh, True, $opts); // 上传图片,自动创建目录 - fclose($fh); - var_dump($rsp); - echo "=========DONE\r\n\r\n"; - - echo "=========按照预先设置的缩略图类型生成缩略图类型生成缩略图,不保存原图,仅对图片空间有效\r\n"; - $opts = array( - UpYun::X_GMKERL_THUMBNAIL => 'thumbtype' - ); - $fh = fopen(__DIR__.'/sample.jpeg', 'rb'); - $rsp = $upyun->writeFile('/demo/sample_thumb_2.jpeg', $fh, True, $opts); // 上传图片,自动创建目录 - fclose($fh); - var_dump($rsp); - echo "=========DONE\r\n\r\n"; -} -catch(Exception $e) { - echo $e->getCode(); - echo $e->getMessage(); -} diff --git a/phpunit.xml b/phpunit.xml index 0e72d02..bdc7acf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,12 +10,9 @@ bootstrap="tests/bootstrap.php"> - ./tests/ + ./tests/SignatureTest.php + ./tests/UpyunTest.php + ./tests/Api/MultiTest.php - - - ./upyun.class.php - - - \ No newline at end of file + diff --git a/src/Upyun/Api/Form.php b/src/Upyun/Api/Form.php new file mode 100644 index 0000000..462ee31 --- /dev/null +++ b/src/Upyun/Api/Form.php @@ -0,0 +1,44 @@ +config->bucketName; + if (!isset($params['expiration'])) { + $params['expiration'] = time() + 30 * 60 * 60; // 30 分钟 + } + + $result = Signature::getFormSignature($this->config, $params); + $policy = $result['policy']; + $signature = $result['signature']; + $client = new Client([ + 'timeout' => $this->config->timeout, + ]); + + $url = ($this->config->useSsl ? 'https://' : 'http://') . $this->endpoint; + + $response = $client->request('POST', $url, array( + 'multipart' => array( + array( + 'name' => 'policy', + 'contents' => $policy, + ), + array( + 'name' => 'signature', + 'contents' => $signature, + ), + array( + 'name' => 'file', + 'contents' => $stream, + ) + ) + )); + return $response->getStatusCode() === 200; + } +} \ No newline at end of file diff --git a/src/Upyun/Api/Multi.php b/src/Upyun/Api/Multi.php new file mode 100644 index 0000000..a9f62fd --- /dev/null +++ b/src/Upyun/Api/Multi.php @@ -0,0 +1,144 @@ +config = $config; + $this->url = ($this->config->useSsl ? 'https://' : 'http://') . Config::ED_FORM . '/'. + $this->config->bucketName; + } + + /** + * @param string $path 文件存储路径 + * @param Psr7\stream $stream 通过 `Psr7\stream_for` 方法格式化的流资源 + * @param string $fileHash 文件 md5 值 + * @param array $params 其他自定义参数 + * + * @return Psr7\Response + * @throws \Exception + */ + public function upload($path, $stream, $fileHash, $params = []) { + $path = '/' . ltrim($path, '/'); + $initInfo = $this->initRequest($path, $stream, $fileHash, $params); + $blockStatus = $initInfo->status; + + $newBlockStatus = $blockStatus; + + for($blockId = 0; $blockId < $initInfo->blocks; $blockId++) { + if($blockStatus[$blockId] === 0) { + $return = $this->blockUpload($initInfo, $blockId, $stream); + $newBlockStatus = $return->status; + } + } + + if(array_sum($newBlockStatus) === $initInfo->blocks) { + return $this->endRequest($initInfo, $params); + } else { + throw new \Exception(sprintf("chunk upload failed! current every block status is : [%s]", implode(',', $newBlockStatus))); + } + } + + private function initRequest($path, Psr7\Stream $stream, $fileHash, $params) { + $metaData = array( + 'expiration' => time() + $this->config->blockExpiration, + 'file_blocks' => ceil($stream->getSize() / $this->config->maxBlockSize), + 'file_hash' => $fileHash, + 'file_size' => $stream->getSize(), + 'path' => $path + ); + + $metaData = array_merge($metaData, $params); + $policy = Util::base64Json($metaData); + $signature = Signature::getSignature( + $this->config, + $metaData, + Signature::SIGN_MULTIPART + ); + $postData = compact('policy', 'signature'); + + $client = new Client(); + $response = $client->request('POST', $this->url, [ + 'form_params' => $postData, + ]); + + $initInfo = json_decode($response->getBody()->getContents()); + return $initInfo; + } + + private function blockUpload($blocksInfo, $blockId, Psr7\Stream $stream, $params = []) { + $startPosition = $blockId * $this->config->maxBlockSize; + $endPosition = $blockId >= $blocksInfo->blocks - 1 ? $stream->getSize() : $startPosition + $this->blockSize; + + $stream->seek($startPosition); + + $fileBlock = $stream->read($endPosition - $startPosition); + + $metaData = array( + 'save_token' => $blocksInfo->save_token, + 'expiration' => $blocksInfo->expired_at, + 'block_index' => $blockId, + 'block_hash' => md5($fileBlock), + ); + $metaData = array_merge($metaData, $params); + $postData['policy'] = Util::base64Json($metaData); + $postData['signature'] = Signature::getSignature( + $this->config, + $metaData, + Signature::SIGN_MULTIPART, + $blocksInfo->token_secret + ); + + $multipart = []; + foreach($postData as $key => $value) { + $multipart[] = ['name' => $key, 'contents' => $value]; + } + $multipart[] = [ + 'name' => 'file', + 'contents' => $fileBlock, + 'filename' => 'file', //this value must be file + 'headers' => ['Content-Type' => 'application/octet-stream'] + ]; + $postData['file'] = $fileBlock; + + $client = new Client(); + $response = $client->request('POST', $this->url, [ + 'multipart' => $multipart, + ]); + + return json_decode($response->getBody()->getContents()); + } + + private function endRequest($initInfo, $data = array()) { + $metaData['save_token'] = $initInfo->save_token; + $metaData['expiration'] = $initInfo->expired_at; + + $metaData = array_merge($metaData, $data); + $policy = Util::base64Json($metaData); + $signature = Signature::getSignature( + $this->config, + $metaData, + Signature::SIGN_MULTIPART, + $initInfo->token_secret + ); + $postData = compact('policy', 'signature'); + + $client = new Client(); + $response = $client->request('POST', $this->url, [ + 'form_params' => $postData + ]); + return $response; + } +} \ No newline at end of file diff --git a/src/Upyun/Api/Pretreat.php b/src/Upyun/Api/Pretreat.php new file mode 100644 index 0000000..65757a6 --- /dev/null +++ b/src/Upyun/Api/Pretreat.php @@ -0,0 +1,72 @@ +config = $config; + } + + public function process($source, $tasks) { + $encodedTasks = Util::base64Json($tasks); + + $client = new Client([ + 'timeout' => $this->config->timeout, + ]); + + $params = array( + 'bucket_name' => $this->config->bucketName, + 'notify_url' => $this->config->processNotifyUrl, + 'source' => $source, + 'tasks' => $encodedTasks, + 'accept' => 'json' + ); + + $url = $this->url . '/pretreatment'; + $signature = Signature::getSignature($this->config, $params, Signature::SIGN_VIDEO); + $response = $client->request('POST', $url, [ + 'headers' => array('Authorization' => "UPYUN {$this->config->operatorName}:$signature"), + 'form_params' => $params + ]); + + $body = $response->getBody()->getContents(); + return json_decode($body, true); + } + + + public function query($taskIds, $path) { + $client = new Client([ + 'timeout' => $this->config->timeout, + ]); + + $params = array( + 'bucket_name' => $this->config->bucketName, + 'task_ids' => implode(',', $taskIds) + ); + + $url = $this->url . $path; + $signature = Signature::getSignature($this->config, $params, Signature::SIGN_VIDEO); + $response = $client->request('GET', $url, [ + 'headers' => array('Authorization' => "UPYUN {$this->config->operatorName}:$signature"), + 'query' => $params + ]); + + if ($response->getStatusCode() === 200) { + $body = $response->getBody()->getContents(); + $result = json_decode($body, true); + if (is_array($result)) { + return $result['tasks']; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/Upyun/Api/Rest.php b/src/Upyun/Api/Rest.php new file mode 100644 index 0000000..3862d4e --- /dev/null +++ b/src/Upyun/Api/Rest.php @@ -0,0 +1,91 @@ +config = $config; + $this->endpoint = Config::$restApiEndPoint . '/' . $config->bucketName; + } + + public function request($method, $storagePath) { + $this->method = strtoupper($method); + $this->storagePath = '/' . ltrim($storagePath, '/'); + return $this; + } + + + /** + * @param string|resource $file + * + * @return $this + */ + public function withFile($file) { + $stream = Psr7\stream_for($file); + $this->file = $stream; + + return $this; + } + + /** + * @return mixed|\Psr\Http\Message\ResponseInterface + */ + public function send() { + $client = new Client([ + 'timeout' => $this->config->timeout, + ]); + + $url = ($this->config->useSsl ? 'https://' : 'http://') . $this->endpoint . $this->storagePath; + $bodySize = 0; + $body = null; + if($this->file && $this->method === 'PUT') { + $bodySize = $this->file->getSize(); + $body = $this->file; + } + + $authHeader = Signature::getRestApiSignHeader($this->config, $this->method, $this->storagePath, $bodySize); + $response = $client->request($this->method, $url, [ + 'headers' => array_merge($authHeader, $this->headers), + 'body' => $body + ]); + + return $response; + } + + public function withHeader($header, $value) { + $header = strtolower(trim($header)); + + $this->headers[$header] = $value; + return $this; + } + + public function withHeaders($headers) { + if(is_array($headers)) { + foreach ($headers as $header => $value) { + $this->withHeader($header, $value); + } + } + return $this; + } +} diff --git a/src/Upyun/Config.php b/src/Upyun/Config.php new file mode 100644 index 0000000..7b554b9 --- /dev/null +++ b/src/Upyun/Config.php @@ -0,0 +1,124 @@ +bucketName = $bucketName; + $this->operatorName = $operatorName; + $this->setOperatorPassword($operatorPassword); + $this->useSsl = false; + self::$restApiEndPoint = self::ED_AUTO; + } + + public function setOperatorPassword($operatorPassword) { + $this->operatorPassword = md5($operatorPassword); + } + + public function getFormApiKey() { + if(! $this->formApiKey) { + throw new \Exception('form api key is empty.'); + } + + return $this->formApiKey; + } + + public function setFormApiKey($key) { + $this->formApiKey = $key; + } + + public function getVersion() { + return $this->version; + } +} diff --git a/src/Upyun/Signature.php b/src/Upyun/Signature.php new file mode 100644 index 0000000..d0ec513 --- /dev/null +++ b/src/Upyun/Signature.php @@ -0,0 +1,101 @@ +bucketName . '/' . ltrim($remotePath, '/'); + + $sign = md5("$method&$path&$gmtDate&$contentLength&{$bucketConfig->operatorPassword}"); + + $headers = array( + 'Authorization' => "UpYun {$bucketConfig->operatorName}:$sign", + 'Date' => $gmtDate, + 'User-agent' => 'Php-Sdk/' . $bucketConfig->getVersion() . ' (rest api)' + ); + return $headers; + } + + /** + * 获取请求缓存刷新接口需要的签名头 + * + * @param Config $bucketConfig + * @param $urlString + * + * @return array + */ + public static function getPurgeSignHeader( Config $bucketConfig, $urlString) { + $gmtDate = gmdate('D, d M Y H:i:s \G\M\T'); + $sign = md5("$urlString&{$bucketConfig->bucketName}&$gmtDate&{$bucketConfig->operatorPassword}"); + return array( + 'Authorization' => "UpYun {$bucketConfig->bucketName}:{$bucketConfig->operatorName}:$sign", + 'Date' => $gmtDate, + 'User-agent' => 'Php-Sdk/' . $bucketConfig->getVersion() . ' (purge api)' + ); + } + + public static function getFormSignature(Config $bucketConfig, $data) { + $data['bucket'] = $bucketConfig->bucketName; + $policy = Util::base64Json($data); + $signature = md5($policy . '&' . $bucketConfig->getFormApiKey()); + return array( + 'policy' => $policy, + 'signature' => $signature + ); + } + + public static function getSignature( Config $bucketConfig, $data, $type, $tokenSecret = '') { + if(is_array($data)) { + ksort($data); + $string = ''; + foreach($data as $k => $v) { + if(is_array($v)) { + $v = implode('', $v); + } + $string .= "$k$v"; + } + switch($type) { + case self::SIGN_MULTIPART: + $string .= $tokenSecret ? $tokenSecret : $bucketConfig->getFormApiKey(); + break; + case self::SIGN_VIDEO: + $string = $bucketConfig->operatorName . $string . $bucketConfig->operatorPassword; + break; + case self::SIGN_VIDEO_NO_OPERATOR: + break; + + } + $sign = md5($string); + return $sign; + } + return false; + } +} \ No newline at end of file diff --git a/src/Upyun/Uploader.php b/src/Upyun/Uploader.php new file mode 100644 index 0000000..5352374 --- /dev/null +++ b/src/Upyun/Uploader.php @@ -0,0 +1,115 @@ +config = $config; + } + + public function upload($path, $file, $params, $withAsyncProcess) { + $stream = Psr7\stream_for($file); + $size = $stream->getSize(); + $useBlock = $this->needUseBlock($size); + + if ($withAsyncProcess) { + $req = new Form($this->config); + return $req->upload($path, $stream, $params); + } + + if(! $useBlock) { + $req = new Rest($this->config); + return $req->request('PUT', $path) + ->withHeaders($params) + ->withFile($stream) + ->send(); + } else { + return $this->pointUpload($path, $stream, $params); + } + } + + /** + * 断点续传 + * @param $path + * @param $stream + * @param $params + * + * @return mixed|\Psr\Http\Message\ResponseInterface + * @throws \Exception + */ + private function pointUpload($path, $stream, $params) { + $req = new Rest($this->config); + $headers = array(); + if (is_array($params)) { + foreach($params as $key => $val) { + $headers['X-Upyun-Meta-' . $key] = $val; + } + } + $res = $req->request('PUT', $path) + ->withHeaders(array_merge(array( + 'X-Upyun-Multi-Stage' => 'initiate', + 'X-Upyun-Multi-Type' => Psr7\mimetype_from_filename($path), + 'X-Upyun-Multi-Length' => $stream->getSize(), + ), $headers)) + ->send(); + if ($res->getStatusCode() !== 204) { + throw new \Exception('init request failed when poinit upload!'); + } + + $init = Util::getHeaderParams($res->getHeaders()); + $uuid = $init['x-upyun-multi-uuid']; + $blockSize = 1024 * 1024; + $partId = 0; + do { + $fileBlock = $stream->read($blockSize); + $res = $req->request('PUT', $path) + ->withHeaders(array( + 'X-Upyun-Multi-Stage' => 'upload', + 'X-Upyun-Multi-Uuid' => $uuid, + 'X-Upyun-Part-Id' => $partId + )) + ->withFile(Psr7\stream_for($fileBlock)) + ->send(); + + if ($res->getStatusCode() !== 204) { + throw new \Exception('upload request failed when poinit upload!'); + } + $data = Util::getHeaderParams($res->getHeaders()); + $partId = $data['x-upyun-next-part-id']; + } while($partId != -1); + + $res = $req->request('PUT', $path) + ->withHeaders(array( + 'X-Upyun-Multi-Uuid' => $uuid, + 'X-Upyun-Multi-Stage' => 'complete' + )) + ->send(); + + if ($res->getStatusCode() != 204 && $res->getStatusCode() != 201) { + throw new \Exception('end request failed when poinit upload!'); + } + return $res; + } + + private function needUseBlock($fileSize) { + if($this->config->uploadType === 'BLOCK') { + return true; + } else if($this->config->uploadType === 'AUTO' && + $fileSize >= $this->config->sizeBoundary ) { + return true; + } else { + return false; + } + } +} diff --git a/src/Upyun/Upyun.php b/src/Upyun/Upyun.php new file mode 100644 index 0000000..e55cb35 --- /dev/null +++ b/src/Upyun/Upyun.php @@ -0,0 +1,352 @@ +setConfig($config); + } + + /** + * 更新服务配置 + * + * 当需要操作的新的服务时,使用该方法传入新的服务配置即可 + * + * @param Config $config 服务配置 + * + * @return $this + */ + public function setConfig(Config $config) { + $this->config = $config; + return $this; + } + + /** + * 上传一个文件到又拍云存储 + * + * 上传的文件格式支持文件流或者字符串方式上传。除简单的文件上传外,针对多媒体资源(图片、音视频),还可以设置同步/异步预处理多媒体资源,例如:图片的裁剪缩放,音视频的转码截图等等众多又拍云强大的云处理功能 + * + * @param string $path 被上传的文件在又拍云存储服务中保存的路径 + * @param string|resource $content 被上传的文件内容(字符串),或者打开该文件获得的文件句柄(文件流)。当上传本地大文件时,推荐使用文件流的方式上传 + * @param array $params 上传文件时,附加的自定义参数。支持 Content-MD5 Content-Type Content-Secret 等,详见 [上传参数](http://docs.upyun + * .com/api/rest_api/#_2),例如: + * - 设置文件[保护秘钥](http://docs.upyun.com/api/rest_api/#Content-Secret) `write($path, $content, array('Content-Secret' => 'my-secret'))`; + * - 添加[文件元信息](http://docs.upyun.com/api/rest_api/#metadata) `write($path, $content, array('X-Upyun-Meta-Foo' => + * 'bar'))` + * - [图片同步预处理](http://docs.upyun.com/cloud/image/#_5) `write($path, $content, array('x-gmkerl-thumb' => '/format/png'))` + * @param bool $withAsyncProcess 默认为 `false`,当上传图片或者音视频资源时,可以设置该参数为 `true`,开启图片音视频的[异步处理功能](http://docs.upyun.com/api/form_api/#_6) ,例如: + *``` + * // 以下参数会将新上传的图片,再异步生成另一份 png 格式的图片,原图不受影响 + * write($path, $content, array( + * 'apps' => array( + * array( + * 'name' => 'thumb', //异步图片处理任务 + * 'x-gmkerl-thumb' => '/format/png', // 格式化图片为 png 格式 + * 'save_as': '/iamge/png/new.png', // 处理成功后的图片保存路径 + * 'notify_url': 'http://your.notify.url' // 异步任务完成后的回调地址 + * ) + * ) + * ), true); + *``` + * + * + * + * @return array|bool 若文件是图片则返回图片基本信息,如:`array('x-upyun-width' => 123, 'x-upyun-height' => 50, 'x-upyun-frames' + * => 1, 'x-upyun-file-type' => 'JPEG')`,否则返回空数组。当使用异步预处理功能时,返回结果为布尔值,成功为 `true`。 + * + * @throws \Exception 上传失败时,抛出异常 + */ + public function write($path, $content, $params = array(), $withAsyncProcess = false) { + if(!$content) { + throw new \Exception('write content can not be empty.'); + } + + $upload = new Uploader($this->config); + $response = $upload->upload($path, $content, $params, $withAsyncProcess); + if ($withAsyncProcess) { + return $response; + } + return Util::getHeaderParams($response->getHeaders()); + } + + /** + * 读取云存储文件/目录内容 + * + * @param string $path 又拍云存储中的文件或者目录路径 + * @param resource $saveHandler 文件内容写入本地文件流。例如 `$saveHandler = fopen('/local/file', 'w') + * `。当设置该参数时,将以文件流的方式,直接将又拍云中的文件写入本地的文件流,或其他可以写入的流 + * @param array $params 可选参数,读取目录内容时,需要设置三个参数: `X-List-Iter` 分页开始位置(第一页不需要设置),`X-List-Limit` 获取的文件数量(默认 100,最大 + * 10000),`X-List-Order` 结果以时间正序或者倒序 + * + * @return mixed $return 当读取文件且没有设置 `$saveHandler` 参数时,返回一个字符串类型,表示文件内容;设置了 `$saveHandler` 参数时,返回布尔值 + * `true`。当读取目录时,返回一个数组,表示目录下的文件列表。目录下文件内容过多时,需要通过判断返回数组中的 `is_end` 属性,进行分页读取内容 + * + * @throws \Exception + */ + public function read($path, $saveHandler = NULL, $params = array()) { + $req = new Rest($this->config); + $response = $req->request('GET', $path) + ->withHeaders($params) + ->send(); + + + $params = Util::getHeaderParams($response->getHeaders()); + + + if(! isset($params['x-upyun-list-iter'])) { + if(is_resource($saveHandler)) { + Psr7\copy_to_stream($response->getBody(), Psr7\stream_for($saveHandler)); + return true; + } else { + return $response->getBody()->getContents(); + } + } else { + $files = Util::parseDir($response->getBody()); + return array('files' => $files, 'is_end' => $params['x-upyun-list-iter'] === 'g2gCZAAEbmV4dGQAA2VvZg', 'iter' => $params['x-upyun-list-iter']); + } + } + + /** + * 判断文件是否存在于又拍云存储 + * + * 注意: 对刚删除的文件, 立即调用该方法可能会返回 true, 因为服务端执行删除操作后可能会有很短暂的延迟. + * + * @param string $path 云存储的文件路径 + * + * @return bool 存在时返回 `true`,否则返回 `false` + * @throws \Exception + */ + public function has($path) { + $req = new Rest($this->config); + try { + $req->request('HEAD', $path) + ->send(); + } catch(GuzzleHttp\Exception\BadResponseException $e) { + $statusCode = $e->getResponse()->getStatusCode(); + if($statusCode === 404) { + return false; + } else { + throw $e; + } + } + + return true; + } + + /** + * 获取云存储文件/目录的基本信息 + * + * @param string $path 云存储的文件路径 + * + * @return array 返回一个数组,包含以下 key + * - `x-upyun-file-type` 当 $path 是目录时,值为 *folder*,当 $path 是文件时,值为 *file*, + * - `x-upyun-file-size` 文件大小 + * - `x-upyun-file-date` 文件的创建时间 + */ + public function info($path) { + $req = new Rest($this->config); + $response = $req->request('HEAD', $path) + ->send(); + return Util::getHeaderParams($response->getHeaders()); + } + + /** + * 删除文件或者目录 + * + * @param string $path 文件或目录在又拍云存储的路径 + * @param bool $async 是否异步删除,默认为 false,表示同步删除。当需要批量删除大量文件时,必须选择异步删除 + * + * @return bool 删除成功返回 true,否则 false + * @throws \Exception 删除不存在的文件将会抛出异常 + */ + public function delete($path, $async = false) { + $req = new Rest($this->config); + $req->request('DELETE', $path); + if($async) { + $req->withHeader('x-upyun-async', 'true'); + } + $res = $req->send(); + return $res->getStatusCode() === 200; + } + + /** + * 创建目录 + * + * @param string $path 需要在又拍云存储创建的目录路径 + * + * @return bool 创建成功返回 true,否则返回 false + * @throws \Exception + */ + public function createDir($path) { + $path = rtrim($path, '/') . '/'; + $req = new Rest($this->config); + $res = $req->request('POST', $path) + ->withHeader('folder', 'true') + ->send(); + return $res->getStatusCode() === 200; + } + + /** + * 删除文件或者目录 + * + * @param string $path 需要被删除的云存储文件或目录路径 + * + * @return bool 成功返回 true,否则 false + * @throws \Exception + */ + public function deleteDir($path) { + return $this->delete($path); + } + + /** + * 获取目录下存储使用量 + * + * @param string $path 云存储目录路径,默认为根目录,表示整个云存储服务使用的空间大小 + * @return string 存储使用量,单位字节 + * @throws \Exception + */ + public function usage($path = '/') { + + $path = rtrim($path, '/') . '/'; + $req = new Rest($this->config); + $response = $req->request('GET', $path . '?usage') + ->withHeader('folder', 'true') + ->send(); + + return $response->getBody()->getContents(); + } + + /** + * 刷新缓存 + * + * @param array|string $urls 需要刷新的文件 url 列表 + * + * @return array 刷新失败的 url 列表,若全部刷新成功则为空数组 + */ + public function purge($urls) { + $urlString = $urls; + if(is_array($urls)) { + $urlString = implode("\n", $urls); + } + + $client = new Client([ + 'timeout' => $this->config->timeout + ]); + $response = $client->request('POST', Config::ED_PURGE, [ + 'headers' => Signature::getPurgeSignHeader($this->config, $urlString), + 'form_params' => ['purge' => $urlString] + ]); + $result = json_decode($response->getBody()->getContents(), true); + return $result['invalid_domain_of_url']; + } + + /** + * 异步云处理 + * + * 该方法是基于[又拍云云处理](http://docs.upyun.com/cloud/) 服务实现,可以实现音视频的转码、切片、剪辑;文件的压缩解压缩;文件拉取功能 + * 所有需要调用该方法处理的资源,必须已经上传到云存储服务,未上传到云存储的文件,同时需要云处理功能,请使用 `write` 方法。 + * 例如视频转码: + * ``` + * process($source, array( + * array( + * 'type' => 'video', // video 表示视频任务, audio 表示音频任务 + * 'avopts' => '/s/240p(4:3)/as/1/r/30', // 处理参数,`s` 表示输出的分辨率,`r` 表示视频帧率,`as` 表示是否自动调整分辨率 + * 'save_as' => '/video/240/new.mp4', // 新视频在又拍云存储的保存路径 + * ), + * ... // 同时还可以添加其他任务 + * )) + * ``` + * 注意,被处理的资源需要已经上传到又拍云云存储 + * + * @param string $source 需要预处理的图片、音视频资源在又拍云存储的路径 + * @param array $tasks 需要处理的任务 + * + * @return array 任务 ID,提交了多少任务,便会返回多少任务 ID,与提交任务的顺序保持一致。可以通过任务 ID 查询处理进度。格式如下: + * ``` + * array( + * '35f0148d414a688a275bf915ba7cebb2', + * '98adbaa52b2f63d6d7f327a0ff223348', + * ) + * ``` + */ + public function process($source, $tasks) { + $video = new Api\Pretreat($this->config); + return $video->process($source, $tasks); + } + + /** + * 音视频预处理任务进度查询 + * + * 根据 `process` 方法返回的任务 ID,通过该访问查询处理进度 + * + * @param array $taskIds 任务 ID + * + * @return bool|array 查询失败返回布尔值 `false`,否则返回每个任务的百分比进度信息,格式如下: + * ``` + * array( + * '35f0148d414a688a275bf915ba7cebb2' => 100, // 100 表示任务完成 + * 'c3103189fa906a5354d29bd807e8dc51' => 35, + * '98adbaa52b2f63d6d7f327a0ff223348' => null, // null 表示任务未开始,或异常 + * ) + * ``` + */ + public function queryProcessStatus($taskIds) { + $video = new Api\Pretreat($this->config); + return $video->query($taskIds, '/status'); + } + + /** + * 音视频预处理任务结果查询 + * + * 根据 `process` 方法返回的任务 ID,通过该访问查询处理结果,会包含每个任务详细信息 + * @param array $taskIds 任务 ID + * + * @return bool|mixed 查询失败返回 `false`,否则返回每个任务的处理结果,格式如下: + * ``` + * array( + * '9d9c32b63a1034834e77672c6f51f661' => array( + * 'path' => array('/v2.mp4'), + * 'signature' => '4042c1f07f546d28', + * 'status_code' => 200, + * 'bucket_name' => 'your_storage_bucket', + * 'description' => 'OK', + * 'task_id' => '9d9c32b63a1034834e77672c6f51f661', + * 'timestamp' => 1472010684 + * ) + * ) + * ``` + */ + public function queryProcessResult($taskIds) { + $video = new Api\Pretreat($this->config); + return $video->query($taskIds, '/result'); + } +} \ No newline at end of file diff --git a/src/Upyun/Util.php b/src/Upyun/Util.php new file mode 100644 index 0000000..c00f922 --- /dev/null +++ b/src/Upyun/Util.php @@ -0,0 +1,59 @@ + $value) { + $header = strtolower($header); + if(strpos($header, 'x-upyun-') !== false) { + $params[$header] = $value[0]; + } + } + return $params; + } + + public static function parseDir($body) { + $files = array(); + if(!$body) { + return array('files' => $files, 'is_end' => true); + } + + $lines = explode("\n", $body); + foreach($lines as $line) { + $file = []; + list($file['name'], $file['type'], $file['size'], $file['time']) = explode("\t", $line, 4); + $files[] = $file; + } + + return $files; + } + + public static function base64Json($params) { + return base64_encode(json_encode($params)); + } + + public static function stringifyHeaders($headers) { + $return = array(); + foreach ($headers as $key => $value) { + $return[] = "$key: $value"; + } + return $return; + } + + public static function md5Hash($resource) { + rewind($resource); + $ctx = hash_init('md5'); + hash_update_stream($ctx, $resource); + $md5 = hash_final($ctx); + return $md5; + } +} \ No newline at end of file diff --git a/src/Upyun/Video.php b/src/Upyun/Video.php new file mode 100644 index 0000000..b107b6b --- /dev/null +++ b/src/Upyun/Video.php @@ -0,0 +1,115 @@ +setConfig($bucketConfig); + } + + public function setConfig(Config $bucketConfig) { + $this->config = $bucketConfig; + } + + public function pretreat($source, $notifyUrl, $tasks) { + $postParams['tasks'] = Util::base64Json($tasks); + $postParams['source'] = $source; + $postParams['notify_url'] = $notifyUrl; + $postParams['bucket_name'] = $this->config->bucketName; + $sign = Signature::getSignature( + $this->config, + $postParams, + Signature::SIGN_VIDEO + ); + + $response = Request::post( + sprintf('http://%s/%s/', Config::ED_VIDEO, 'pretreatment'), + array('Authorization' => "UpYun {$this->config->operatorName}:$sign"), + $postParams + ); + + if($response->status_code !== 200) { + $body = json_decode($response->body, true); + throw new \Exception(sprintf('%s, with x-request-id=%s', $body['msg'], $body['id']), $body['code']); + } + + + $taskIds = json_decode($response->body, true); + return $taskIds; + } + + + public function status($taskIds) { + $limit = 20; + if(count($taskIds) <= $limit) { + $taskIds = implode(',', $taskIds); + } else { + throw new \Exception('can not query more than ' . $limit . ' tasks at one time!'); + } + + $query['task_ids'] = $taskIds; + $query['bucket_name'] = $this->config->bucketName; + $sign = Signature::getSignature( + $this->config, + $query, + Signature::SIGN_VIDEO + ); + + $response = Request::get( + sprintf('http://%s/%s/', Config::ED_VIDEO, 'status'), + array('Authorization' => "UpYun {$this->config->operatorName}:$sign"), + $query + ); + + if($response->status_code !== 200) { + $body = json_decode($response->body, true); + throw new \Exception(sprintf('%s, with x-request-id=%s', $body['msg'], $body['id']), $body['code']); + } + + $status = json_decode($response->body, true); + return $status; + } + + public function callbackSignVerify() { + $callbackKeys = array( + 'bucket_name', + 'status_code', + 'path', + 'description', + 'task_id', + 'info', + 'signature', + ); + $callbackParams = array(); + foreach($callbackKeys as $key) { + if(isset($_POST[$key])) { + $callbackParams[$key] = Util::trim($_POST[$key]); + } + } + + if(isset($callbackParams['signature'])) { + $sign = $callbackParams['signature']; + unset($callbackParams['signature']); + return $sign === Signature::getSignature( + $this->config, + $callbackParams, + Signature::SIGN_VIDEO + ); + } + + if(isset($data['non_signature'])) { + $sign = $callbackParams['non_signature']; + unset($callbackParams['non_signature']); + return $sign === Signature::getSignature( + $this->config, + $callbackParams, + Signature::SIGN_VIDEO_NO_OPERATOR + ); + } + return false; + } +} \ No newline at end of file diff --git a/tests/Api/MultiTest.php b/tests/Api/MultiTest.php new file mode 100644 index 0000000..dce72ff --- /dev/null +++ b/tests/Api/MultiTest.php @@ -0,0 +1,28 @@ +setFormApiKey('Mv83tlocuzkmfKKUFbz2s04FzTw='); + $this->multiPart = new Multi($config); + } + + public function testUpload() { + $filePath = __DIR__ . '/../assets/sample.jpeg'; + $stream = Psr7\stream_for(fopen($filePath, 'rb')); + $r = $this->multiPart->upload('test-sample.jpeg', $stream, md5_file($filePath)); + $this->assertEquals($r->getStatusCode(), 200); + } +} diff --git a/tests/SignatureTest.php b/tests/SignatureTest.php new file mode 100644 index 0000000..0625bd7 --- /dev/null +++ b/tests/SignatureTest.php @@ -0,0 +1,21 @@ +config = new Config('bucket', 'operator', 'password'); + } + + public function testGetSignature() { + $sign = Signature::getSignature($this->config, array('a' => 'a', 'b' => 'b'), Signature::SIGN_MULTIPART, '123'); + $this->assertEquals($sign , '2aa0afd612df8fab4b3fded36c396234'); + } +} \ No newline at end of file diff --git a/tests/UpyunTest.php b/tests/UpyunTest.php new file mode 100644 index 0000000..049852e --- /dev/null +++ b/tests/UpyunTest.php @@ -0,0 +1,218 @@ +setFormApiKey('Mv83tlocuzkmfKKUFbz2s04FzTw='); + $config->processNotifyUrl = 'http://localhost:9999'; + self::$upyun = new Upyun($config); + self::$tempFilePath = __DIR__ . '/assets/test.txt'; + touch(self::$tempFilePath); + } + + public static function tearDownAfterClass() { + unlink(self::$tempFilePath); + } + + public function testWriteString() { + $filename = 'test.txt'; + $content = 'test file content'; + self::$upyun->write($filename, $content); + $size = getUpyunFileSize($filename); + $this->assertEquals($size, strlen($content)); + } + + public function testWriteStream() { + $filename = 'test.jpeg'; + $f = fopen(__DIR__ . '/assets/sample.jpeg', 'rb'); + if(!$f) { + throw new \Exception('open test file failed!'); + } + self::$upyun->write($filename, $f); + $size = getUpyunFileSize($filename); + $this->assertEquals($size, PIC_SIZE); + } + + public function testWriteWithAsyncProcess() { + $filename = 'test_async.jpeg'; + $newFilename = 'test_async.png'; + $f = fopen(__DIR__ . '/assets/sample.jpeg', 'rb'); + if(!$f) { + throw new \Exception('open test file failed!'); + } + $result = self::$upyun->write($filename, $f, array( + 'apps' => array( + array( + 'name' => 'thumb', + 'x-gmkerl-thumb' => '/format/png/fw/50', + 'save_as' => $newFilename, + ) + ) + ), true); + $size = getUpyunFileSize($filename); + $this->assertEquals($size, PIC_SIZE); + $this->assertEquals($result, true); + } + + public function testWriteWithException() { + $fs = new Upyun(new Config(BUCKET, USER_NAME, 'error-password')); + try { + $fs->write('test.txt', 'test file content'); + } catch(\Exception $e) { + return ; + } + throw new \Exception('should get sign error.'); + } + + /** + * @depends testWriteString + */ + public function testReadFile() { + $name = 'test-read.txt'; + $str = 'test file content 2'; + self::$upyun->write($name, $str); + + //读取内容写入字符串 + $content = self::$upyun->read($name); + $this->assertEquals($content, $str); + + //读取内容写入文件流 + $this->assertTrue(self::$upyun->read($name, fopen(self::$tempFilePath, 'wb'))); + $this->assertEquals($str, file_get_contents(self::$tempFilePath)); + } + + /** + * @depends testWriteString + * @depends testReadFile + */ + public function testDeleteFile() { + self::$upyun->write('test-delete.txt', 'test file content 3'); + $r = self::$upyun->delete('test-delete.txt'); + try { + self::$upyun->read('test-delete.txt'); + } catch(\Exception $e) { + return ; + } + throw new \Exception('delete file failed'); + } + + /** + * @expectedException \Exception + */ + public function testDeleteNotExistsFile() { + self::$upyun->delete('not-exists-test.txt'); + } + + /** + */ + public function testHas() { + $name = 'test-has.txt'; + self::$upyun->write($name, 'test file content 4'); + $this->assertEquals(self::$upyun->has($name), true); + self::$upyun->delete($name); + sleep(5); + $this->assertEquals(self::$upyun->has($name), false); + } + + /** + * @depends testWriteString + * @depends testDeleteFile + */ + public function testInfo() { + self::$upyun->write('test-info.txt', 'test file content 4'); + $info = self::$upyun->info('test-info.txt'); + $this->assertEquals($info['x-upyun-file-type'], 'file'); + $this->assertEquals($info['x-upyun-file-size'], 19); + } + + /** + */ + public function testCreateDir() { + self::$upyun->createDir('/test-dir'); + $this->assertEquals(self::$upyun->has('/test-dir'), true); + self::$upyun->createDir('/test-dir2/'); + $this->assertEquals(self::$upyun->has('/test-dir2'), true); + } + + public function testReadDir() { + $list = self::$upyun->read('/test-dir2/'); + $this->assertEquals($list['is_end'], true); + self::$upyun->write('/test-dir2/test.txt', 'test file content 5'); + $list = self::$upyun->read('/test-dir2/'); + $this->assertEquals($list['is_end'], true); + $this->assertEquals(count($list['files']), 1); + $file = $list['files'][0]; + $this->assertEquals($file['name'], 'test.txt'); + $this->assertEquals($file['type'], 'N'); + $this->assertEquals($file['size'], 19); + } + + /** + * @depends testCreateDir + */ + public function testDeleteDir() { + $result = self::$upyun->createDir('/test-delete-dir'); + $this->assertEquals($result, true); + sleep(5); + $result = self::$upyun->deleteDir('/test-delete-dir'); + $this->assertEquals($result, true); + } + + public function testUsage() { + $size = self::$upyun->usage(); + $this->assertTrue($size > 0); + } + + public function testPurge() { + $urls = self::$upyun->purge(getFileUrl('test.txt')); + $this->assertTrue(empty($urls)); + + $invalidUrl = 'http://xxxx.b0.xxxxxxxx-upyun.com/test.txt'; + $urls = self::$upyun->purge($invalidUrl); + $this->assertTrue(count($urls) === 1); + $this->assertTrue($urls[0] === $invalidUrl); + } + + public function testProcess() { + $source = 'php-sdk-sample.mp4'; + self::$upyun->write($source, fopen(__DIR__ . '/assets/SampleVideo_640x360_1mb.mp4', 'r')); + $result = self::$upyun->process($source, array( + array('type' => 'video', 'avopts' => '/s/240p(4:3)/as/1/r/30', 'return_info' => true, 'save_as' => '/video/result.mp4') + )); + $this->assertTrue(strlen($result[0]) === 32); + self::$taskId = $result[0]; + } + + /** + * @depends testProcess + */ + public function testQueryProcessStatus() { + sleep(5); + $status = self::$upyun->queryProcessStatus(array(self::$taskId)); + $this->assertTrue(array_key_exists(self::$taskId, $status)); + } + + /** + * @depends testProcess + */ + public function testQueryProcessResult() { + sleep(5); + $result = self::$upyun->queryProcessResult(array(self::$taskId)); + $this->assertTrue($result[self::$taskId]['path'][0] === '/video/result.mp4'); + $this->assertTrue($result[self::$taskId]['status_code'] === 200); + } +} \ No newline at end of file diff --git a/tests/assets/SampleVideo_640x360_1mb.mp4 b/tests/assets/SampleVideo_640x360_1mb.mp4 new file mode 100644 index 0000000..02c2060 Binary files /dev/null and b/tests/assets/SampleVideo_640x360_1mb.mp4 differ diff --git a/tests/assets/sample.jpeg b/tests/assets/sample.jpeg index bbb067d..dbfe17d 100644 Binary files a/tests/assets/sample.jpeg and b/tests/assets/sample.jpeg differ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d781b60..031cf9c 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,9 +1,30 @@ 'tester', - 'pwd' => 'grjxv2mxELR3', - 'bucket' => 'sdkimg', - 'picture_path' => dirname(__FILE__) . '/assets/sample.jpeg' - ); \ No newline at end of file +return array( + 'user_name' => 'tester', + 'pwd' => 'grjxv2mxELR3', + 'bucket' => 'sdkimg', + 'picture_path' => dirname(__FILE__) . '/assets/sample.jpeg' +); \ No newline at end of file diff --git a/tests/upyunTest.php b/tests/upyunTest.php deleted file mode 100644 index 7b1a033..0000000 --- a/tests/upyunTest.php +++ /dev/null @@ -1,99 +0,0 @@ -upyun = new UpYun(BUCKET, USER_NAME, PWD, UpYun::ED_TELECOM, 600); - } - - public function testMakeDir() - { - $rsp = $this->upyun->makeDir('/demo/'); - $this->assertTrue(true); - } - - /** - * 直接上传文件 - */ - public function testDirectUpload() - { - $fh = fopen(PIC_PATH, 'rb'); - $rsp = $this->upyun->writeFile('/demo/sample_normal.jpeg', $fh, True); // 上传图片,自动创建目录 - fclose($fh); - $this->assertTrue(true, is_array($rsp)); - } - - /** - * 直接生成缩略图,不保存原图片,仅对图片文件有效 - */ - public function testWriteFile1() - { - $opts = array( - UpYun::X_GMKERL_TYPE => 'square', // 缩略图类型 - UpYun::X_GMKERL_VALUE => 150, // 缩略图大小 - UpYun::X_GMKERL_QUALITY => 95, // 缩略图压缩质量 - UpYun::X_GMKERL_UNSHARP => True // 是否进行锐化处理 - ); - $fh = fopen(PIC_PATH, 'rb'); - $rsp = $this->upyun->writeFile('/demo/sample_thumb_1.jpeg', $fh, True, $opts); // 上传图片,自动创建目录 - fclose($fh); - $this->assertTrue(is_array($rsp)); - } - - /** - * 获取空间的使用情况 - */ - public function testUsage() - { - $rsp = $this->upyun->getFolderUsage('/demo/'); - $this->assertTrue(is_string($rsp)); - } - - /** - * 获取指定文件的目录信息 - */ - public function testFileInfo() - { - $rsp = $this->upyun->getFileInfo('/demo/sample_normal.jpeg'); - $this->assertArrayHasKey('x-upyun-file-type',$rsp); - $this->assertArrayHasKey('x-upyun-file-size',$rsp); - $this->assertArrayHasKey('x-upyun-file-date',$rsp); - } - - /** - * 获取目录文件列表 - */ - public function testList() - { - $rsp = $this->upyun->getList('/demo/'); - $this->assertTrue(is_array($rsp)); - } - - /** - * 删除空间目录 - * @expectedException \Exception - * @depends testMakeDir - */ - public function testDelete() - { - $rsp = $this->upyun->delete('/demo/'); - $this->assertTrue($rsp); - } - - /** - * 获取错误请求的 X-Request-Id - */ - public function testXRequestId() - { - $rsp = $this->upyun->getList('/demo/'); - $x_id = $this->upyun->getXRequestId(); - $this->assertEquals(strlen($x_id), 32); - } -} diff --git a/upyun.class.php b/upyun.class.php deleted file mode 100644 index a7b6813..0000000 --- a/upyun.class.php +++ /dev/null @@ -1,444 +0,0 @@ -_bucketname = $bucketname; - $this->_username = $username; - $this->_password = md5($password); - $this->_timeout = $timeout; - - $this->endpoint = is_null($endpoint) ? self::ED_AUTO : $endpoint; - } - - /** - * 获取当前SDK版本号 - */ - public function version() - { - return self::VERSION; - } - - /** - * 创建目录 - * @param $path string 路径 - * @param $auto_mkdir bool 是否自动创建父级目录,最多10层次 - * - * @return mixed - */ - public function makeDir($path, $auto_mkdir = true) - { - $headers = array('Folder' => 'true'); - if ($auto_mkdir) $headers['Mkdir'] = 'true'; - return $this->_do_request('PUT', $path, $headers); - } - - /** - * 删除目录和文件 - * @param string $path 路径 - * - * @return boolean - */ - public function delete($path) - { - return $this->_do_request('DELETE', $path); - } - - - /** - * 上传文件 - * @param string $path 存储路径 - * @param mixed $file 需要上传的文件,可以是文件流或者文件内容 - * @param boolean $auto_mkdir 自动创建目录 - * @param array $opts 可选参数 - * @return mixed|null - */ - public function writeFile($path, $file, $auto_mkdir = true, $opts = NULL) - { - if (is_null($opts)) $opts = array(); - - if (!is_null($this->_content_md5)) $opts[self::CONTENT_MD5] = $this->_content_md5; - if (!is_null($this->_file_secret)) $opts[self::CONTENT_SECRET] = $this->_file_secret; - - if ($auto_mkdir === true) $opts['Mkdir'] = 'true'; - - return $this->_do_request('PUT', $path, $opts, $file); - } - - /** - * 下载文件 - * @param string $path 文件路径 - * @param mixed $file_handle - * - * @return mixed - */ - public function readFile($path, $file_handle = NULL) - { - return $this->_do_request('GET', $path, NULL, NULL, $file_handle); - } - - /** - * 获取目录文件列表 - * - * @param string $path 查询路径 - * - * @return mixed - */ - public function getList($path = '/') - { - $rsp = $this->_do_request('GET', $path); - - $list = array(); - if ($rsp) { - $rsp = explode("\n", $rsp); - foreach ($rsp as $item) { - @list($name, $type, $size, $time) = explode("\t", trim($item)); - if (!empty($time)) { - $type = ($type == 'N') ? 'file' : 'folder'; - } - - $item = array( - 'name' => $name, - 'type' => $type, - 'size' => intval($size), - 'time' => intval($time), - ); - array_push($list, $item); - } - } - - return $list; - } - - /** - * 获取文件、目录信息 - * - * @param string $path 路径 - * - * @return mixed - */ - public function getFileInfo($path) - { - $rsp = $this->_do_request('HEAD', $path); - return $rsp; - } - - /** - * 获取空间使用情况 - * @param string $bucket - * @return mixed - * @throws UpYunAuthorizationException - * @throws UpYunException - * @throws UpYunForbiddenException - * @throws UpYunNotAcceptableException - * @throws UpYunNotFoundException - * @throws UpYunServiceUnavailable - */ - public function getFolderUsage($bucket = '/') - { - return $this->_do_request('GET', "{$bucket}?usage"); - } - - /** - * 获取空间存储使用量,单位 byte - */ - public function getBucketUsage() - { - return $this->getFolderUsage('/'); - } - - public function getXRequestId() - { - return $this->x_request_id; - } - - /** - * 设置文件访问密钥 - */ - public function setFileSecret($str) - { - $this->_file_secret = $str; - } - - /** - * 这是文件 md5 校验值 - */ - public function setContentMd5($str) - { - $this->_content_md5 = $str; - } - - /** - * 连接签名方法 - * @param $method string 请求方式 {GET, POST, PUT, DELETE} - * @return string 签名字符串 - */ - private function sign($method, $uri, $date, $length) - { - //$uri = urlencode($uri); - $sign = "{$method}&{$uri}&{$date}&{$length}&{$this->_password}"; - return 'UpYun ' . $this->_username . ':' . md5($sign); - } - - /** - * HTTP REQUEST 封装 - * @param string $method HTTP REQUEST方法,包括PUT、POST、GET、OPTIONS、DELETE - * @param string $path 除Bucketname之外的请求路径,包括get参数 - * @param array $headers 请求需要的特殊HTTP HEADERS - * @param array $body 需要POST发送的数据 - * @param null $file_handle - * @return mixed - * @throws UpYunAuthorizationException - * @throws UpYunException - * @throws UpYunForbiddenException - * @throws UpYunNotAcceptableException - * @throws UpYunNotFoundException - * @throws UpYunServiceUnavailable - */ - protected function _do_request($method, $path, $headers = NULL, $body = NULL, $file_handle = NULL) - { - $uri = "/{$this->_bucketname}{$path}"; - $ch = curl_init("http://{$this->endpoint}{$uri}"); - - $_headers = array('Expect:'); - if (!is_null($headers) && is_array($headers)) { - foreach ($headers as $k => $v) { - array_push($_headers, "{$k}: {$v}"); - } - } - - $length = 0; - $date = gmdate('D, d M Y H:i:s \G\M\T'); - - if (!is_null($body)) { - if (is_resource($body)) { - fseek($body, 0, SEEK_END); - $length = ftell($body); - fseek($body, 0); - - array_push($_headers, "Content-Length: {$length}"); - curl_setopt($ch, CURLOPT_INFILE, $body); - curl_setopt($ch, CURLOPT_INFILESIZE, $length); - } else { - $length = @strlen($body); - array_push($_headers, "Content-Length: {$length}"); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - } - } else { - array_push($_headers, "Content-Length: {$length}"); - } - - array_push($_headers, "Authorization: {$this->sign($method, $uri, $date, $length)}"); - array_push($_headers, "Date: {$date}"); - - curl_setopt($ch, CURLOPT_HTTPHEADER, $_headers); - curl_setopt($ch, CURLOPT_TIMEOUT, $this->_timeout); - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - //curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - - if ($method == 'PUT' || $method == 'POST') { - curl_setopt($ch, CURLOPT_POST, 1); - } else { - curl_setopt($ch, CURLOPT_POST, 0); - } - - if ($method == 'GET' && is_resource($file_handle)) { - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_FILE, $file_handle); - } - - if ($method == 'HEAD') { - curl_setopt($ch, CURLOPT_NOBODY, true); - } - - $response = curl_exec($ch); - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - - if ($http_code == 0) throw new UpYunException('Connection Failed', $http_code); - - curl_close($ch); - - $header_string = ''; - $body = ''; - - if ($method == 'GET' && is_resource($file_handle)) { - $header_string = ''; - $body = $response; - } else { - list($header_string, $body) = explode("\r\n\r\n", $response, 2); - } - $this->setXRequestId($header_string); - if ($http_code == 200) { - if ($method == 'GET' && is_null($file_handle)) { - return $body; - } else { - $data = $this->_getHeadersData($header_string); - return count($data) > 0 ? $data : true; - } - } else { - $message = $this->_getErrorMessage($header_string); - if (is_null($message) && $method == 'GET' && is_resource($file_handle)) { - $message = 'File Not Found'; - } - switch ($http_code) { - case 401: - throw new UpYunAuthorizationException($message); - break; - case 403: - throw new UpYunForbiddenException($message); - break; - case 404: - throw new UpYunNotFoundException($message); - break; - case 406: - throw new UpYunNotAcceptableException($message); - break; - case 503: - throw new UpYunServiceUnavailable($message); - break; - default: - throw new UpYunException($message, $http_code); - } - } - } - - /** - * 处理HTTP HEADERS中返回的自定义数据 - * - * @param string $text header字符串 - * - * @return array - */ - private function _getHeadersData($text) - { - $headers = explode("\r\n", $text); - $items = array(); - foreach ($headers as $header) { - $header = trim($header); - if (stripos($header, 'x-upyun') !== False) { - list($k, $v) = explode(':', $header); - $items[trim($k)] = in_array(substr($k, 8, 5), array('width', 'heigh', 'frame')) ? intval($v) : trim($v); - } - } - return $items; - } - - /** - * 获取返回的错误信息 - * - * @param string $header_string - * - * @return mixed - */ - private function _getErrorMessage($header_string) - { - list($status, $stash) = explode("\r\n", $header_string, 2); - list($v, $code, $message) = explode(" ", $status, 3); - return $message . " X-Request-Id: " . $this->getXRequestId(); - } - - private function setXRequestId($header_string) - { - preg_match('~^X-Request-Id: ([0-9a-zA-Z]{32})~ism', $header_string, $result); - $this->x_request_id = isset($result[1]) ? $result[1] : ''; - } -} - - -class UpYunException extends Exception -{ - public function __construct($message, $code, Exception $previous = null) - { - parent::__construct($message, $code); // For PHP 5.2.x - } - - public function __toString() - { - return __CLASS__ . ": [{$this->code}]: {$this->message}\n"; - } -} - -class UpYunAuthorizationException extends UpYunException -{ - public function __construct($message, $code = 0, Exception $previous = null) - { - parent::__construct($message, 401, $previous); - } -} - -class UpYunForbiddenException extends UpYunException -{ - public function __construct($message, $code = 0, Exception $previous = null) - { - parent::__construct($message, 403, $previous); - } -} - -class UpYunNotFoundException extends UpYunException -{ - public function __construct($message, $code = 0, Exception $previous = null) - { - parent::__construct($message, 404, $previous); - } -} - -class UpYunNotAcceptableException extends UpYunException -{ - public function __construct($message, $code = 0, Exception $previous = null) - { - parent::__construct($message, 406, $previous); - } -} - -class UpYunServiceUnavailable extends UpYunException -{ - public function __construct($message, $code = 0, Exception $previous = null) - { - parent::__construct($message, 503, $previous); - } -}