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

-又拍云存储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
+
+
+
+
+
+
+
+
+
+
+
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);
- }
-}