diff --git a/.gitignore b/.gitignore index 217f1b38..72a03e60 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,3 @@ results node_modules npm-debug.log -demo/*.jpg -demo/rtest.js -demo/itest.js -demo/put.sh diff --git a/.npmignore b/.npmignore index c68978b7..3867aec4 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,7 @@ test/ +test-env.sh .travis.yml coverage.html lib-cov/ Makefile docs/ -demo/ diff --git a/.travis.yml b/.travis.yml index ad2aee43..a0331ae1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ language: node_js node_js: + - 0.10 - 0.8 - 0.6 before_script: - export QINIU_ACCESS_KEY="nnwjTeUgpQdfZp9cb4-iHK0EUlebKCNk4kXwoStq" - export QINIU_SECRET_KEY="Ia9pXC-XEcGF6hvu1V5fdRhwFLpeUkCbt0Gxk5NW" + - export QINIU_TEST_BUCKET="test741" + - export QINIU_TEST_DOMAIN="test741.qiniudn.com" diff --git a/CHANGELOG.md b/CHANGELOG.md index 30aa8d89..42fc4233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,67 +1,7 @@ -#CHANGELOG +## CHANGE LOG -## v2.4.3 +### v6.0.0 -2013-02-22 - -Issue [#54](https://github.com/qiniu/nodejs-sdk/pull/54): - -- 去除对自定义 generateQueryString() 函数的依赖,使用 querystring 包的 stringify() 函数。 -- auth.PutPolicy 增加对 returnBody 的支持,用户可自定义上传完文件后的返回值。 - -## v2.4.2 - -Issue [#48](https://github.com/qiniu/nodejs-sdk/pull/48): - -- 去除 auth.GetPolicy.scope 的默认值:"*/*" - -Issue [#46](https://github.com/qiniu/nodejs-sdk/pull/46): - -- 更新 v2.4.1 的文档(docs/README.md) - - -## v2.4.1 - -2013-02-09 - -Issue [#43](https://github.com/qiniu/nodejs-sdk/pull/43): - -- imageMogr bugfix: auto-orient -- auth.UploadToken, auth.DownloadToken 改为 auth.PutPolicy, auth.GetPolicy -- auth.UploadToken.generateToken() 改为 auth.PutPolicy.token() -- auth.DownloadToken.generateToken() 改为 auth.GetPolicy.token() -- auth.DownloadToken.pattern 改为 auth.GetPolicy.scope - - -## v2.4.0 - -2013-01-23 - -Issue [#36](https://github.com/qiniu/nodejs-sdk/pull/36): - -- 增加 auth.DownloadToken 类 -- auth.UploadToken 增加:escape、asyncOps 成员,generateSignature 改名为 generateToken -- 增加 rs.copy, rs.move, rs.batchGet, rs.batchStat, rs.batchDelete, rs.batchCopy, rs.batchMove -- 增加 Travis-CI 的支持 - -Issue [#32](https://github.com/qiniu/nodejs-sdk/pull/32): - -- auth.UploadToken.generateSignature 各个参数调整为可选 -- uploadWithToken 非兼容调整: rs.uploadWithToken(uploadToken, stream, key, mimeType, customMeta, callbackParams, crc32, onret) -- generateActionString 非兼容调整: action = util.generateActionString(bucket, key, mimeType, customMeta, crc32) - - -## v2.3.2 - -2012-12-31 - -- 修复crc32编码 -- 修复使用UploadToken方式上传时流式上传bug,流式上传不检查crc32 - - -## v2.3.0 - -2012-11-23 - -- 启用新的 uploadToken(上传凭证)上传方式,可由客户方业务服务器生成上传凭证。上传前无需请求七牛云存储,减少http请求。 +2013-07-16 issue [#56](https://github.com/qiniu/nodejs-sdk/pull/56) +- 遵循 [sdkspec v6.0.4](https://github.com/qiniu/sdkspec/tree/v6.0.4) diff --git a/Makefile b/Makefile index 1911dead..20868608 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,25 @@ TESTS = test/*.test.js +TIMEOUT = 15000 REPORTER = spec -TIMEOUT = 10000 +MOCHA_OPTS = +test: + @NODE_ENV=test ./node_modules/.bin/mocha \ + --require should \ + --reporter $(REPORTER) \ + --timeout $(TIMEOUT) \ + $(MOCHA_OPTS) \ + $(TESTS) -test: - @NODE_ENV=test ./node_modules/mocha/bin/mocha \ - --reporter $(REPORTER) \ - --timeout $(TIMEOUT) \ - $(TESTS) - -test-cov: - @rm -rf ./lib-cov - @$(MAKE) lib-cov - @QINIU_COV=1 $(MAKE) test REPORTER=dot +test-cov: lib-cov +# @QINIU_COV=1 $(MAKE) test REPORTER=dot @QINIU_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html + @rm -rf ./lib-cov + lib-cov: - @jscoverage lib $@ + @jscoverage --no-highlight qiniu $@ + +clean: + rm -rf ./lib-cov coverage.html -.PHONY: test-cov test lib-cov +.PHONY: test-cov lib-cov test diff --git a/README.md b/README.md index 620271c9..0ac540a0 100644 --- a/README.md +++ b/README.md @@ -1,125 +1,34 @@ -# Node.js wrapper for Qiniu Resource (Cloud) Storage API +# Qiniu Resource Storage SDK for Node.js [![Build Status](https://travis-ci.org/qiniu/nodejs-sdk.png?branch=master)](https://travis-ci.org/qiniu/nodejs-sdk) -![logo](http://qiniutek.com/images/logo-2.png) +[![Qiniu Logo](http://qiniutek.com/images/logo-2.png)](http://qiniu.com/) -该 SDK 适用于 Node.js 0.4.7 及其以上版本,基于 [七牛云存储官方API](/v3/api/) 构建。若您的服务端是一个基于 Node.js 编写的网络程序,使用此 SDK ,能让您以非常便捷地方式将数据安全地存储到七牛云存储上。以便让您应用的终端用户进行高速上传和下载,同时也使得您的服务端更加轻盈。 +## 下载 -jscoverage: [85%](http://fengmk2.github.com/coverage/qiniu.html) +### 从 npm 安装 -## 安装 +这是我们建议的方式 - npm install qiniu +``` +npm install qiniu +``` -### 获取 ACCESS_KEY 和 SECRET_KEY +### 从 release 版本下载 -要对接七牛云存储服务,您需要七牛云存储服务端颁发给您的 `ACCESS_KEY` 和 `SECRET_KEY`。`ACCESS_KEY` 用于标识客户方的身份,在网络请求中会以某种形式进行传输。`SECRET_KEY` 作为私钥形式存放于客户方本地并不在网络中传递,`SECRET_KEY` 的作用是对于客户方发起的具体请求进行数字签名,用以保证该请求是来自指定的客户方并且请求本身是合法有效的。使用 `ACCESS_KEY` 进行身份识别,加上 `SECRET_KEY` 进行数字签名,即可完成应用接入与认证授权。 +下载地址:https://github.com/qiniu/nodejs-sdk/releases -您可以通过如下步骤获得 `ACCESS_KEY` 和 `SECRET_KEY`: +这里可以下载到旧版本的SDK,release 版本有版本号,有 [CHANGELOG](https://github.com/qiniu/nodejs-sdk/blob/develop/CHANGELOG.md),使用规格也会比较稳定。 -1. [开通七牛开发者帐号](https://dev.qiniutek.com/signup) -2. [登录七牛开发者自助平台,查看 ACCESS_KEY 和 SECRET_KEY](https://dev.qiniutek.com/account/keys) +### 从 git 库下载 -获取到 `ACCESS_KEY` 和 `SECRET_KEY` 之后,您就可以参考下面的示例代码进行接入使用了。 +你可以直接用 git clone 下载源代码来使用。但是请注意非 master 分支的代码在规格上可能承受变更,应谨慎使用。 ## 使用 -SDK 使用文档参考:[http://docs.qiniutek.com/v3/sdk/nodejs/](http://docs.qiniutek.com/v3/sdk/nodejs/) - -### 示例程序 - - var qiniu = require('qiniu'); - - // 配置密钥 - qiniu.conf.ACCESS_KEY = ''; - qiniu.conf.SECRET_KEY = ''; - - // 实例化带授权的 HTTP Client 对象 - var conn = new qiniu.digestauth.Client(); - - // 创建空间,也可以在开发者自助网站创建 - var bucket = 'yet_another_bucket'; - qiniu.rs.mkbucket(conn, bucket, function(resp) { - console.log("\n===> Make bucket result: ", resp); - if (resp.code != 200) { - return; - } - }); - - // 实例化 Bucket 操作对象 - var rs = new qiniu.rs.Service(conn, bucket); - - // 上传文件第1步 - // 生成上传授权凭证(uploadToken) - var opts = { - scope: "yet_another_bucket", // 可以是 "" 或 ":" - expires: 3600, - callbackUrl: "http://www.example.com/notifications/qiniurs", // 可选 - callbackBodyType: "application/x-www-form-urlencoded", // 可选 - }; - var uploadPolicy = new qiniu.auth.PutPolicy(opts); - var uploadToken = uploadPolicy.token(); - - // 上传文件第2步 - // 组装上传文件所需要的参数 - var key = __filename; - var localFile = key, - customMeta = "", - callbackParams = {"bucket": bucket, "key": key}, - enableCrc32Check = false, - mimeType = mime.lookup(key); - - // 上传文件第3步 - // 上传文件 - rs.uploadFileWithToken(uploadToken, localFile, key, mimeType, customMeta, callbackParams, enableCrc32Check, function(resp){ - console.log("\n===> Upload File with Token result: ", resp); - if (resp.code != 200) { - // ... - return; - } - - // 查看已上传文件属性信息 - rs.stat(key, function(resp) { - console.log("\n===> Stat result: ", resp); - if (resp.code != 200) { - // ... - return; - } - }); - }); - - - // 获取文件下载链接(含文件属性信息) - var saveAsFriendlyName = key; - rs.get(key, saveAsFriendlyName, function(resp) { - console.log("\n===> Get result: ", resp); - if (resp.code != 200) { - // ... - return; - } - }); - - // 删除已上传文件 - rs.remove(key, function(resp) { - console.log("\n===> Delete result: ", resp); - }); - - // 将bucket的内容作为静态内容发布 - var DEMO_DOMAIN = bucket + '.dn.qbox.me'; - rs.publish(DEMO_DOMAIN, function(resp){ - console.log("\n===> Publish result: ", resp); - if (resp.code != 200){ - clear(rs); - return; - } - }); - - // 删除bucket,慎用! - rs.drop(function(resp){ - console.log("\n===> Drop result: ", resp); - }); +参考文档:[七牛云存储 Node.js SDK 使用指南](https://github.com/qiniu/nodejs-sdk/tree/develop/docs) +旧版本的SDK(version < 6.0.0) [戳这里](http://docs.qiniutek.com/v3/sdk/nodejs/) ## 贡献代码 @@ -131,7 +40,7 @@ SDK 使用文档参考:[http://docs.qiniutek.com/v3/sdk/nodejs/](http://docs.q ## 许可证 -Copyright (c) 2012 qiniu.com +Copyright (c) 2013 qiniu.com 基于 MIT 协议发布: diff --git a/docs/README.gist.md b/docs/README.gist.md new file mode 100644 index 00000000..d36eb940 --- /dev/null +++ b/docs/README.gist.md @@ -0,0 +1,364 @@ +--- +title: Node.js SDK 使用指南 +--- + + + +SDK 下载地址:[https://github.com/qiniu/nodejs-sdk](https://github.com/qiniu/nodejs-sdk) + +**文档大纲** + +- [概述](#overview) +- [准备开发环境](#prepare) + - [环境依赖](#dependences) + - [安装](#install) + - [ACCESS_KEY 和 SECRET_KEY](#appkey) +- [使用SDK](#sdk-usage) + - [初始化环境与清理](#init) + - [上传文件](#io-put) + - [上传流程](#io-put-flow) + - [上传策略](#io-put-policy) + - [上传凭证](#upload-token) + - [PutExtra](#put-extra) + - [下载文件](#io-get) + - [下载公有文件](#io-get-public) + - [下载私有文件](#io-get-private) + - [HTTPS 支持](#io-https-get) + - [断点续下载](#resumable-io-get) + - [资源操作](#rs) + - [获取文件信息](#rs-stat) + - [删除文件](#rs-delete) + - [复制/移动文件](#rs-copy-move) + - [批量操作](#rs-batch) + - [云处理](#fop) + + +## 概述 + + +该 SDK 适用于 Node.js 0.4.7 及其以上版本,基于 七牛云存储官方API 构建。 +若您的服务端是一个基于 Node.js 编写的网络程序,使用此 SDK , +能让您以非常便捷地方式将数据安全地存储到七牛云存储上。 +以便让您应用的终端用户进行高速上传和下载,同时也使得您的服务端更加轻盈。 + + +Node.js SDK 主要包含对七牛云存储API的包装,遵循[qiniu sdkspec](https://github.com/qiniu/sdkspec) +涉及到以下几个方面: + +- 服务端操作,生成上传授权(uptoken),私有bucket下载URL(downloadUrl),文件操作授权 +- 客户端操作,上传文件(qiniu/io.js) +- 文件管理(qiniu/rs.js) +- 数据处理(qiniu/fop.js) +- 公共库(qiniu/rpc.js, qiniu/util.js) + + + +## 准备开发环境 + + + + + +### 环境依赖 + +适用于 Node.js 0.4.7 及其以上版本 + + + +### 安装 + + +通过 npm 以 node 模块化的方式安装: +`npm node qiniu` + + + +### ACCESS_KEY 和 SECRET_KEY + +在使用SDK 前,您需要拥有一对有效的 AccessKey 和 SecretKey 用来进行签名授权。 + +可以通过如下步骤获得: + +1. [开通七牛开发者帐号](https://portal.qiniu.com/signup) +2. [登录七牛开发者自助平台,查看 AccessKey 和 SecretKey](https://portal.qiniu.com/setting/key) 。 + + + +## 使用SDK + + + +### 初始化环境 + +对于服务端而言,常规程序流程是: + +```{javascript} +@gist(gist/server.js#init) +``` + +*服务端操作时请务必初始化这两个变量* + + + +### 上传文件 + +为了尽可能地改善终端用户的上传体验,七牛云存储首创了客户端直传功能。一般云存储的上传流程是: + + 客户端(终端用户) => 业务服务器 => 云存储服务 + +这样多了一次上传的流程,和本地存储相比,会相对慢一些。但七牛引入了客户端直传,将整个上传过程调整为: + + 客户端(终端用户) => 七牛 => 业务服务器 + +客户端(终端用户)直接上传到七牛的服务器,通过DNS智能解析,七牛会选择到离终端用户最近的ISP服务商节点,速度会比本地存储快很多。文件上传成功以后,七牛的服务器使用回调功能,只需要将非常少的数据(比如Key)传给应用服务器,应用服务器进行保存即可。 + + + +#### 上传流程 + +在七牛云存储中,整个上传流程大体分为这样几步: + +1. 业务服务器颁发 [uptoken(上传授权凭证)](http://docs.qiniu.com/api/put.html#uploadToken)给客户端(终端用户) +2. 客户端凭借 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 上传文件到七牛 +3. 在七牛获得完整数据后,发起一个 HTTP 请求回调到业务服务器 +4. 业务服务器保存相关信息,并返回一些信息给七牛 +5. 七牛原封不动地将这些信息转发给客户端(终端用户) + +需要注意的是,回调到业务服务器的过程是可选的,它取决于业务服务器颁发的 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken)。如果没有回调,七牛会返回一些标准的信息(比如文件的 hash)给客户端。如果上传发生在业务服务器,以上流程可以自然简化为: + +1. 业务服务器生成 uptoken(不设置回调,自己回调到自己这里没有意义) +2. 凭借 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 上传文件到七牛 +3. 善后工作,比如保存相关的一些信息 + + + +##### 上传策略 + +[uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 实际上是用 AccessKey/SecretKey 进行数字签名的上传策略(`rs.PutPolicy`),它控制则整个上传流程的行为。让我们快速过一遍你都能够决策啥: + +```{javascript} +@gist(../qiniu/rs.js#PutPolicy) +``` + +* `scope` 限定客户端的权限。如果 `scope` 是 bucket,则客户端只能新增文件到指定的 bucket,不能修改文件。如果 `scope` 为 bucket:key,则客户端可以修改指定的文件。 +* `callbackUrl` 设定业务服务器的回调地址,这样业务服务器才能感知到上传行为的发生。 +* `callbackBody` 设定业务服务器的回调信息。文件上传成功后,七牛向业务服务器的callbackUrl发送的POST请求携带的数据。支持 [魔法变量](http://docs.qiniu.com/api/put.html#MagicVariables) 和 [自定义变量](http://docs.qiniu.com/api/put.html#xVariables)。 +* `returnUrl` 设置用于浏览器端文件上传成功后,浏览器执行301跳转的URL,一般为 HTML Form 上传时使用。文件上传成功后浏览器会自动跳转到 `returnUrl?upload_ret=returnBody`。 +* `returnBody` 可调整返回给客户端的数据包,支持 [魔法变量](http://docs.qiniu.com/api/put.html#MagicVariables) 和 [自定义变量](http://docs.qiniu.com/api/put.html#xVariables)。`returnBody` 只在没有 `callbackUrl` 时有效(否则直接返回 `callbackUrl` 返回的结果)。不同情形下默认返回的 `returnBody` 并不相同。在一般情况下返回的是文件内容的 `hash`,也就是下载该文件时的 `etag`;但指定 `returnUrl` 时默认的 `returnBody` 会带上更多的信息。 +* `asyncOps` 可指定上传完成后,需要自动执行哪些数据处理。这是因为有些数据处理操作(比如音视频转码)比较慢,如果不进行预转可能第一次访问的时候效果不理想,预转可以很大程度改善这一点。 +* `expires`指定`uptoken`的过期时间,默认3600s + +关于上传策略更完整的说明,请参考 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken)。 + + + +##### 生成上传凭证 + +服务端生成 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 代码如下: + +```{javascript} +@gist(gist/server.js#uptoken) +``` + + + +##### PutExtra + + + + +PutExtra是上传时的可选信息,默认为null + +```{javascript} +@gist(../qiniu/io.js#PutExtra) +``` + +* `params` 是一个字典。[自定义变量](http://docs.qiniu.com/api/put.html#xVariables),key必须以 x: 开头命名,不限个数。可以在 uploadToken 的 callbackBody 选项中求值。 +* `mime_type` 表示数据的MimeType,当不指定时七牛服务器会自动检测。 +* `crc32` 待检查的crc32值 +* `check_crc` 可选值为0, 1, 2。 + `check_crc == 0`: 表示不进行 crc32 校验。 + `check_crc == 1`: 上传二进制数据时等同于 `check_crc=2`;上传本地文件时会自动计算 crc32 值。 + `check_crc == 2`: 表示进行 crc32 校验,且 crc32 值就是上面的 `crc32` 变量 + +##### 上传文件 + +上传文件到七牛(通常是客户端完成,但也可以发生在服务端): + +直接上传二进制流: + +```{javascript} +@gist(gist/client.js#uploadBuf) +``` + +上传本地文件: + +```{javascript} +@gist(gist/client.js#uploadFile) +``` + + + +#### 下载文件 + + + +##### 下载公有文件 + +每个 bucket 都会绑定一个或多个域名(domain)。如果这个 bucket 是公开的,那么该 bucket 中的所有文件可以通过一个公开的下载 url 可以访问到: + + http:/// + +假设某个 bucket 既绑定了七牛的二级域名,如 hello.qiniudn.com,也绑定了自定义域名(需要备案),如 hello.com。那么该 bucket 中 key 为 a/b/c.htm 的文件可以通过 http://hello.qiniudn.com/a/b/c.htm 或 http://hello.com/a/b/c.htm 中任意一个 url 进行访问。 + + + +##### 下载私有文件 + +如果某个 bucket 是私有的,那么这个 bucket 中的所有文件只能通过一个的临时有效的 downloadUrl 访问: + + http:///?e=&token= + +其中 dntoken 是由业务服务器签发的一个[临时下载授权凭证](http://docs.qiniu.com/api/get.html#download-token),deadline 是 dntoken 的有效期。dntoken不需要单独生成,SDK 提供了生成完整 downloadUrl 的方法(包含了 dntoken),示例代码如下: + +```{javascript} +@gist(gist/server.js#downloadUrl) +``` + +生成 downloadUrl 后,服务端下发 downloadUrl 给客户端。客户端收到 downloadUrl 后,和公有资源类似,直接用任意的 HTTP 客户端就可以下载该资源了。唯一需要注意的是,在 downloadUrl 失效却还没有完成下载时,需要重新向服务器申请授权。 + +无论公有资源还是私有资源,下载过程中客户端并不需要七牛 SDK 参与其中。 + + + +##### 断点续下载 + +无论是公有资源还是私有资源,获得的下载 url 支持标准的 HTTP 断点续传协议。考虑到多数语言都有相应的断点续下载支持的成熟方法,七牛 Nodejs-SDK 并不提供断点续下载相关代码。 + + + +### 资源操作 + +资源操作限在服务端操作,先进行初始化 + +```{javascript} +@gist(gist/rs.js#init) +``` + + + +#### 获取文件信息 + + + +```{javascript} +@gist(gist/rs.js#stat) +``` + + + +#### 删除文件 + +```{javascript} +@gist(gist/rs.js#remove) +``` + + + +#### 复制/移动文件 + +```{javascript} +@gist(gist/rs.js#copy) +``` + +```{javascript} +@gist(gist/rs.js#move) +``` + + + +#### 批量操作 + +当您需要一次性进行多个操作时, 可以使用批量操作。 +批量操作的几个函数中,当返回值`err`为`null`的时候,有两种情况: + +1. 批量操作的所有操作都成功 +2. 批量操作仅仅部分成功 + +因此在返回成功的时候还需要检查各个子项目的返回值(详细见各个示例)。 +注意:批量操作不是对单个文件操作的包装,而是有独立的接口。 + +#### 批量获取文件信息 + +```{javascript} +@gist(gist/rs.js#batchStat) +``` + +#### 批量复制文件 + +```{javascript} +@gist(gist/rs.js#batchCopy) +``` + +#### 批量移动文件 + +```{javascript} +@gist(gist/rs.js#batchMove) +``` + +#### 批量删除文件 + +```{javascript} +@gist(gist/rs.js#batchDelete) +``` + + + +### 高级管理操作 + + +#### 列出文件 +请求某个存储空间(bucket)下的文件列表,如果有前缀,可以按前缀(prefix)进行过滤;第一次调用时置marker为null,之后的调用填上服务器返回的marker(如果有),则列出刚刚为列完的文件 + +```{javascript} +@gist(gist/rsf.js#listPrefix) +``` + + + +### 云处理 + + +#### 查看图像信息 + +```{javascript} +@gist(gist/fop.js#makeImageInfoUrl) +``` + +#### 查看图像Exif + +```{javascript} +@gist(gist/fop.js#makeExifUrl) +``` + +#### 生成缩略图 + +```{javascript} +@gist(gist/fop.js#makeImageViewUrl) +``` + +## 贡献代码 + ++ Fork ++ 创建您的特性分支 (git checkout -b my-new-feature) ++ 提交您的改动 (git commit -am 'Added some feature') ++ 将您的修改记录提交到远程 git 仓库 (git push origin my-new-feature) ++ 然后到 github 网站的该 git 远程仓库的 my-new-feature 分支下发起 Pull Request + +## 许可证 + +> Copyright (c) 2013 qiniu.com + +## 基于 MIT 协议发布: + +> [www.opensource.org/licenses/MIT](http://www.opensource.org/licenses/MIT) diff --git a/docs/README.md b/docs/README.md index 9ae3b729..ff18a924 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,517 +1,598 @@ --- -title: Node.js SDK +title: Node.js SDK 使用指南 --- -该 SDK 适用于 Node.js 0.4.7 及其以上版本,基于 [七牛云存储官方API](/v3/api/) 构建。若您的服务端是一个基于 Node.js 编写的网络程序,使用此 SDK ,能让您以非常便捷地方式将数据安全地存储到七牛云存储上。以便让您应用的终端用户进行高速上传和下载,同时也使得您的服务端更加轻盈。 + -七牛云存储 Node.js SDK 开放源代码地址:[https://github.com/qiniu/nodejs-sdk](https://github.com/qiniu/nodejs-sdk) +SDK 下载地址:[https://github.com/qiniu/nodejs-sdk](https://github.com/qiniu/nodejs-sdk) **文档大纲** -- [安装](#Installation) -- [使用](#Usage) - - [获取 ACCESS_KEY 和 SECRET_KEY](#appkey) - - [应用接入与初始化](#establish_connection!) - - [上传文件](#upload) - - [获取用于上传文件的临时授权凭证](#generate-token) - - [服务端上传文件](#server-side-upload) - - [非断点续传方式](#normal-upload) - - [默认上传方式](#default-upload) - - [针对NotFound处理场景](#upload-file-not-found) - - [客户端上传文件](#client-side-upload) - - [获取文件属性信息](#stat) - - [获取文件下载链接(含文件属性信息)](#get) - - [获取文件下载链接(断点续下载)](#getIfNotModified) - - [创建公开外链](#publish) - - [取消公开外链](#unpublish) - - [删除指定文件](#remove) - - [删除所有文件(指定 bucket)](#drop) - - [图像处理](#fo-image) - - [图像处理(缩略、裁剪、旋转、转化)](#qiniu-img-mogrify) - - [图像处理(缩略、裁剪、旋转、转化)并持久化](#imageMogrifyAs) -- [贡献代码](#Contributing) -- [许可证](#License) - - - -## 安装 - -通过 npm 以 node 模块化的方式安装: - - npm install qiniu - - - -## 使用 +- [概述](#overview) +- [准备开发环境](#prepare) + - [环境依赖](#dependences) + - [安装](#install) + - [ACCESS_KEY 和 SECRET_KEY](#appkey) +- [使用SDK](#sdk-usage) + - [初始化环境与清理](#init) + - [上传文件](#io-put) + - [上传流程](#io-put-flow) + - [上传策略](#io-put-policy) + - [上传凭证](#upload-token) + - [PutExtra](#put-extra) + - [下载文件](#io-get) + - [下载公有文件](#io-get-public) + - [下载私有文件](#io-get-private) + - [HTTPS 支持](#io-https-get) + - [断点续下载](#resumable-io-get) + - [资源操作](#rs) + - [获取文件信息](#rs-stat) + - [删除文件](#rs-delete) + - [复制/移动文件](#rs-copy-move) + - [批量操作](#rs-batch) + - [云处理](#fop) + + +## 概述 + + +该 SDK 适用于 Node.js 0.4.7 及其以上版本,基于 七牛云存储官方API 构建。 +若您的服务端是一个基于 Node.js 编写的网络程序,使用此 SDK , +能让您以非常便捷地方式将数据安全地存储到七牛云存储上。 +以便让您应用的终端用户进行高速上传和下载,同时也使得您的服务端更加轻盈。 + + +Node.js SDK 主要包含对七牛云存储API的包装,遵循[qiniu sdkspec](https://github.com/qiniu/sdkspec) +涉及到以下几个方面: + +- 服务端操作,生成上传授权(uptoken),私有bucket下载URL(downloadUrl),文件操作授权 +- 客户端操作,上传文件(qiniu/io.js) +- 文件管理(qiniu/rs.js) +- 数据处理(qiniu/fop.js) +- 公共库(qiniu/rpc.js, qiniu/util.js) + + + +## 准备开发环境 + + + + + +### 环境依赖 + +适用于 Node.js 0.4.7 及其以上版本 + + + +### 安装 + + +通过 npm 以 node 模块化的方式安装: +`npm node qiniu` -### 获取 ACCESS_KEY 和 SECRET_KEY +### ACCESS_KEY 和 SECRET_KEY -要对接七牛云存储服务,您需要七牛云存储服务端颁发给您的 `ACCESS_KEY` 和 `SECRET_KEY`。`ACCESS_KEY` 用于标识客户方的身份,在网络请求中会以某种形式进行传输。`SECRET_KEY` 作为私钥形式存放于客户方本地并不在网络中传递,`SECRET_KEY` 的作用是对于客户方发起的具体请求进行数字签名,用以保证该请求是来自指定的客户方并且请求本身是合法有效的。使用 `ACCESS_KEY` 进行身份识别,加上 `SECRET_KEY` 进行数字签名,即可完成应用接入与认证授权。 +在使用SDK 前,您需要拥有一对有效的 AccessKey 和 SecretKey 用来进行签名授权。 -您可以通过如下步骤获得 `ACCESS_KEY` 和 `SECRET_KEY`: +可以通过如下步骤获得: 1. [开通七牛开发者帐号](https://portal.qiniu.com/signup) -2. [登录七牛开发者自助平台,查看 ACCESS_KEY 和 SECRET_KEY](https://portal.qiniu.com/setting/key) +2. [登录七牛开发者自助平台,查看 AccessKey 和 SecretKey](https://portal.qiniu.com/setting/key) 。 -获取到 `ACCESS_KEY` 和 `SECRET_KEY` 之后,您就可以参考下面将要介绍的用法进行接入使用了。 + - +## 使用SDK -### 应用接入与初始化 + -使用 npm 安装成功后,您就可以直接在项目中使用了,如下示例代码: +### 初始化环境 - var qiniu = require("qiniu"); +对于服务端而言,常规程序流程是: - // 配置密钥 - qiniu.conf.ACCESS_KEY = ''; - qiniu.conf.SECRET_KEY = ''; +```{javascript} +qiniu.conf.ACCESS_KEY = '' +qiniu.conf.SECRET_KEY = '' +``` - // 实例化带授权的 HTTP Client 对象 - var conn = new qiniu.digestauth.Client(); - var bucket = ""; +*服务端操作时请务必初始化这两个变量* - // 创建空间,也可以在开发者自助网站创建 - qiniu.rs.mkbucket(conn, bucket, function(resp) { - console.log("\n===> Make bucket result: ", resp); - if (resp.code != 200) { - return; - } - }); - - // 实例化 Bucket 操作对象 - var rs = new qiniu.rs.Service(conn, ""); - - - + ### 上传文件 - - -#### 获取用于上传文件的临时授权凭证 - -要上传一个文件,首先需要调用 SDK 提供的 `qiniu.auth.PutPolicy(options)`创建一个PutPolicy对象,然后使用它提供的token()方法生成用于临时匿名上传的upload_token——经过数字签名的一组数据信息,该 upload_token 作为文件上传流中 multipart/form-data 的一部分进行传输。 +为了尽可能地改善终端用户的上传体验,七牛云存储首创了客户端直传功能。一般云存储的上传流程是: + 客户端(终端用户) => 业务服务器 => 云存储服务 - var options = { - scope: , // 可以是 "" 或 ":" - expires: , - callbackUrl: , // 可选 - callbackBodyType: , // 可选 - customer: // 可选 - }; +这样多了一次上传的流程,和本地存储相比,会相对慢一些。但七牛引入了客户端直传,将整个上传过程调整为: - var uploadPolicy = new qiniu.auth.PutPolicy(options); - var uploadToken = uploadPolicy.token(); + 客户端(终端用户) => 七牛 => 业务服务器 -**options参数** +客户端(终端用户)直接上传到七牛的服务器,通过DNS智能解析,七牛会选择到离终端用户最近的ISP服务商节点,速度会比本地存储快很多。文件上传成功以后,七牛的服务器使用回调功能,只需要将非常少的数据(比如Key)传给应用服务器,应用服务器进行保存即可。 -scope -: 必须,字符串类型(String),设定文件要上传到的目标 `bucket`,也可以限定上传的目标必须为 `bucket:key`。 + -expires -: 可选,数字类型,用于设置上传 URL 的有效期,单位:秒,缺省为 3600 秒,即 1 小时后该上传链接不再有效(但该上传URL在其生成之后的59分59秒都是可用的)。 +#### 上传流程 -:callbackUrl -: 可选,字符串类型(String),用于设置文件上传成功后,七牛云存储服务端要回调客户方的业务服务器地址。 +在七牛云存储中,整个上传流程大体分为这样几步: -callbackBodyType -: 可选,字符串类型(String),用于设置文件上传成功后,七牛云存储服务端向客户方的业务服务器发送回调请求的 `Content-Type`。比如发送POST类型的表单数据回调,可以是 `application/x-www-form-urlencoded`。 +1. 业务服务器颁发 [uptoken(上传授权凭证)](http://docs.qiniu.com/api/put.html#uploadToken)给客户端(终端用户) +2. 客户端凭借 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 上传文件到七牛 +3. 在七牛获得完整数据后,发起一个 HTTP 请求回调到业务服务器 +4. 业务服务器保存相关信息,并返回一些信息给七牛 +5. 七牛原封不动地将这些信息转发给客户端(终端用户) -customer -: 可选,字符串类型(String),客户方终端用户(End User)的ID,该字段可以用来标示一个文件的属主,这在一些特殊场景下(比如给终端用户上传的图片打上名字水印)非常有用。 +需要注意的是,回调到业务服务器的过程是可选的,它取决于业务服务器颁发的 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken)。如果没有回调,七牛会返回一些标准的信息(比如文件的 hash)给客户端。如果上传发生在业务服务器,以上流程可以自然简化为: -**响应** +1. 业务服务器生成 uptoken(不设置回调,自己回调到自己这里没有意义) +2. 凭借 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 上传文件到七牛 +3. 善后工作,比如保存相关的一些信息 -返回一个字符串类型(String)的用于上传文件用的临时授权 `uploadToken`。 + +##### 上传策略 - +[uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 实际上是用 AccessKey/SecretKey 进行数字签名的上传策略(`rs.PutPolicy`),它控制则整个上传流程的行为。让我们快速过一遍你都能够决策啥: -#### 服务端上传文件 +```{javascript} +function PutPolicy(scope, callbackUrl, callbackBody, returnUrl, returnBody, + asyncOps, endUser, expires) { + this.scope = scope || null; + this.callbackUrl = callbackUrl || null; + this.callbackBody = callbackBody || null; + this.returnUrl = returnUrl || null; + this.returnBody = returnBody || null; + this.asyncOps = asyncOps || null; + this.endUser = endUser || null; + this.expires = expires || 3600; +} +``` - +* `scope` 限定客户端的权限。如果 `scope` 是 bucket,则客户端只能新增文件到指定的 bucket,不能修改文件。如果 `scope` 为 bucket:key,则客户端可以修改指定的文件。 +* `callbackUrl` 设定业务服务器的回调地址,这样业务服务器才能感知到上传行为的发生。 +* `callbackBody` 设定业务服务器的回调信息。文件上传成功后,七牛向业务服务器的callbackUrl发送的POST请求携带的数据。支持 [魔法变量](http://docs.qiniu.com/api/put.html#MagicVariables) 和 [自定义变量](http://docs.qiniu.com/api/put.html#xVariables)。 +* `returnUrl` 设置用于浏览器端文件上传成功后,浏览器执行301跳转的URL,一般为 HTML Form 上传时使用。文件上传成功后浏览器会自动跳转到 `returnUrl?upload_ret=returnBody`。 +* `returnBody` 可调整返回给客户端的数据包,支持 [魔法变量](http://docs.qiniu.com/api/put.html#MagicVariables) 和 [自定义变量](http://docs.qiniu.com/api/put.html#xVariables)。`returnBody` 只在没有 `callbackUrl` 时有效(否则直接返回 `callbackUrl` 返回的结果)。不同情形下默认返回的 `returnBody` 并不相同。在一般情况下返回的是文件内容的 `hash`,也就是下载该文件时的 `etag`;但指定 `returnUrl` 时默认的 `returnBody` 会带上更多的信息。 +* `asyncOps` 可指定上传完成后,需要自动执行哪些数据处理。这是因为有些数据处理操作(比如音视频转码)比较慢,如果不进行预转可能第一次访问的时候效果不理想,预转可以很大程度改善这一点。 +* `expires`指定`uptoken`的过期时间,默认3600s -##### 非断点续上传方式 +关于上传策略更完整的说明,请参考 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken)。 -如果您确定客户端上传的东西无需使用断点续上传方式进行上传,可以使用rs.uploadFileWithToken()。 + - rs.uploadFileWithToken(uploadToken, localFile, key, mimeType, customMeta, callbackParams, enableCrc32Check, function(resp){ - console.log("\n===> Upload File with Token result: ", resp); - if (resp.code != 200) { - return; - } - }); +##### 生成上传凭证 +服务端生成 [uptoken](http://docs.qiniu.com/api/put.html#uploadToken) 代码如下: -**参数** +```{javascript} +function uptoken(bucketname) { + var putPolicy = new qiniu.rs.PutPolicy(bucketname); + //putPolicy.callbackUrl = callbackUrl; + //putPolicy.callbackBody = callbackBody; + //putPolicy.returnUrl = returnUrl; + //putPolicy.returnBody = returnBody; + //putPolicy.asyncOps = asyncOps; + //putPolicy.expires = expires; -uploadToken -: 必须,字符串类型(String),调用 `PutPolicy.token()` 生成的 [用于上传文件的临时授权凭证](#generate-token) + return putPolicy.token(); +} +``` -localFile -: 必须,字符串类型(String),本地文件可被读取的有效路径 + -key -: 必须,字符串类型(String),类似传统数据库里边某个表的主键ID,给每一个文件一个UUID用于进行标示。 +##### PutExtra -mimeType -: 可选,字符串类型(String),文件的 mime-type 值。如若不传入,SDK 会自行计算得出,若计算失败缺省使用 `application/octet-stream` 代替之。 + -customMeta -: 可选,字符串类型(String),为文件添加备注信息。 + +PutExtra是上传时的可选信息,默认为null -callbackParams -: 可选,String 或者 Hash 类型,文件上传成功后,七牛云存储向客户方业务服务器发送的回调参数。 +```{javascript} +function PutExtra(params, mimeType, crc32, checkCrc) { + this.paras = params || {}; + this.mimeType = mimeType || null; + this.crc32 = crc32 || null; + this.checkCrc = checkCrc || 0; +} +``` -enableCrc32Check -: 可选,Boolean 类型,是否启用文件上传 crc32 校验,缺省为 false 。 +* `params` 是一个字典。[自定义变量](http://docs.qiniu.com/api/put.html#xVariables),key必须以 x: 开头命名,不限个数。可以在 uploadToken 的 callbackBody 选项中求值。 +* `mime_type` 表示数据的MimeType,当不指定时七牛服务器会自动检测。 +* `crc32` 待检查的crc32值 +* `check_crc` 可选值为0, 1, 2。 + `check_crc == 0`: 表示不进行 crc32 校验。 + `check_crc == 1`: 上传二进制数据时等同于 `check_crc=2`;上传本地文件时会自动计算 crc32 值。 + `check_crc == 2`: 表示进行 crc32 校验,且 crc32 值就是上面的 `crc32` 变量 -**响应** +##### 上传文件 -如果操作成功,回调函数的 resp 参数返回如下一段 json 信息: - - { - code: 200, - data: { - hash: 'FrOXNat8VhBVmcMF3uGrILpTu8Cs' - } +上传文件到七牛(通常是客户端完成,但也可以发生在服务端): + +直接上传二进制流: + +```{javascript} +function uploadBuf(body, key, uptoken) { + var extra = new qiniu.io.PutExtra(); + //extra.params = params; + //extra.mimeType = mimeType; + //extra.crc32 = crc32; + //extra.checkCrc = checkCrc; + + qiniu.io.put(uptoken, key, body, extra, function(err, ret) { + if (!err) { + // 上传成功, 处理返回值 + console.log(ret.key, ret.hash); + // ret.key & ret.hash + } else { + // 上传失败, 处理返回代码 + console.log(err) + // http://docs.qiniu.com/api/put.html#error-code } - - - -##### 默认上传方式 - -up.Upload()函数封装了以上断点续上传和非断点续上传的方式。如果您上传的文件大于设置的BLOCK大小(该值可以在conf.js配置文件中进行设置),则默认采用断点续上传的方式进行上传。否则,采用普通的方式进行上传。 - - - -##### 针对 NotFound 场景处理 - -您可以上传一个应对 HTTP 404 出错处理的文件,当您 [创建公开外链](#publish) 后,若公开的外链找不到该文件,即可使用您上传的“自定义404文件”代替之。要这么做,您只须使用 `up.Upload()` 函数上传一个 `key` 为固定字符串类型的值 `errno-404` 即可。 - -除了使用 SDK 提供的方法,同样也可以借助七牛云存储提供的命令行辅助工具 [qboxrsctl](https://github.com/qiniu/devtools/tags) 达到同样的目的: - - qboxrsctl put - -将其中的 `` 换作 `errno-404` 即可。 - -注意,每个 `` 里边有且只有一个 `errno-404` 文件,上传多个,最后的那一个会覆盖前面所有的。 - - - -#### 客户端直传文件 - -客户端上传流程和服务端上传类似,差别在于:客户端直传文件所需的 `uploadToken` 可以选择在客户方的业务服务器端生成,也可以选择在客户方的客户端程序里边生成。选择前者,可以和客户方的业务揉合得更紧密和安全些,比如防伪造请求。 - -简单来讲,客户端上传流程也分为两步: - -1. 获取 `uploadToken`([用于上传文件的临时授权凭证](#generate-upload-token)) -2. 将该 `uploadToken` 作为文件上传流 `multipart/form-data` 中的一部分实现上传操作 - -如果您的网络程序是从云端(服务端程序)到终端(手持设备应用)的架构模型,且终端用户有使用您移动端App上传文件(比如照片或视频)的需求,可以把您服务器得到的此 `uploadToken` 返回给手持设备端的App,然后您的移动 App 可以使用 [七牛云存储 Objective-SDK (iOS)](http://docs.qiniu.com/ios-sdk/index.html) 或 [七牛云存储 Android-SDK](http://docs.qiniu.com/android-sdk/index.html) 的相关上传函数或参照 [七牛云存储API之文件上传](http://docs.qiniu.com/api/put.html) 直传文件。这样,您的终端用户即可把数据(比如图片或视频)直接上传到七牛云存储服务器上无须经由您的服务端中转,而且在上传之前,七牛云存储做了智能加速,终端用户上传数据始终是离他物理距离最近的存储节点。当终端用户上传成功后,七牛云存储服务端会向您指定的 `callbackUrl` 发送回调数据。如果 `callbackUrl` 所在的服务处理完毕后输出 `JSON` 格式的数据,七牛云存储服务端会将该回调请求所得的响应信息原封不动地返回给终端应用程序。 - - - -### 获取文件属性信息 - - rs.stat(key, function(resp) { - console.log("\n===> Stat result: ", resp); - if (resp.code != 200) { - return; - } - }); - -**参数** - -key -: 资源ID - -callback function -: 请求完成之后执行的回调函数 - -**响应** - -如果操作成功,回调函数的 resp 参数返回如下一段 json 信息: - - { - code: 200, - data: { - fsize: 1275, // 资源大小 - hash: 'FrOXNat8VhBVmcMF3uGrILpTu8Cs', // 资源摘要值 - mimeType: 'application/octet-stream', // 资源的 MIME 类型 - putTime: 13421490912350790 // 资源最后修改时间,单位:百纳秒 - } - } - - - -### 获取文件下载链接(含文件属性信息) - - rs.get(key, saveAsFriendlyName, function(resp) { - console.log("\n===> Get result: ", resp); - if (resp.code != 200) { - return; - } - }); - -**参数** - -key -: 资源ID - -saveAsFriendlyName -: 指定文件下载下来要保存的文件名称 - -callback function -: 请求完成之后执行的回调函数 - -**响应** - -如果操作成功,回调函数的 resp 参数返回如下一段 json 信息: - - { - code: 200, - data: { - fsize: 1275, // 资源大小 - hash: 'FrOXNat8VhBVmcMF3uGrILpTu8Cs', // 资源摘要值 - mimeType: 'application/octet-stream', // 资源的 MIME 类型 - expires: 3600 // 缺省3600秒,指定下载链接的有效期 - url: 'http://iovip.qbox.me/file/...' // 文件下载链接 - } + }); +} +``` + +上传本地文件: + +```{javascript} +function uploadFile(localFile, key, uptoken) { + var extra = new qiniu.io.PutExtra(); + //extra.params = params; + //extra.mimeType = mimeType; + //extra.crc32 = crc32; + //extra.checkCrc = checkCrc; + + qiniu.io.putFile(uptoken, key, localFile, extra, function(err, ret) { + if(!err) { + // 上传成功, 处理返回值 + console.log(ret.key, ret.hash); + // ret.key & ret.hash + } else { + // 上传失败, 处理返回代码 + console.log(err); + // http://docs.qiniu.com/api/put.html#error-code } + }); +} +``` - - -### 获取文件下载链接(断点续下载) - - rs.getIfNotModified(key, saveAsFriendlyName, baseVer, function(resp) { - console.log("\n===> Get result: ", resp); - if (resp.code != 200) { - return; - } - }); - -**参数** - -key -: 资源ID - -saveAsFriendlyName -: 指定文件下载下来要保存的文件名称 - -baseVer -: 续传的基版本,一般为上一次请求下载返回的 `hash` 值 - -callback function -: 请求完成之后执行的回调函数 - -**响应** + -同 `rs.get` 返回的结果规格一致。 +#### 下载文件 - + -### 创建公开外链 +##### 下载公有文件 - rs.publish(domain, function(resp) { - console.log("\n===> publish result: ", resp); - if (resp.code != 200) { - return; - } - }); +每个 bucket 都会绑定一个或多个域名(domain)。如果这个 bucket 是公开的,那么该 bucket 中的所有文件可以通过一个公开的下载 url 可以访问到: -调用 `rs.publish` 函数可以将您在七牛云存储中的资源表 `bucket` 发布到某个 `domain` 下,`domain` 需要在 DNS 管理里边 CNAME 到 `iovip.qbox.me` 。 + http:/// -这样,用户就可以通过 `http:///` 来访问资源表 `bucket` 中的文件。键值为 `foo/bar/file` 的文件对应访问 URL 为 `http:///foo/bar/file`。 `domain` 可以是一个真实的域名,比如 `www.example.com`,也可以是七牛云存储的二级路径,比如 `io.qbox.me/bucket` 。 +假设某个 bucket 既绑定了七牛的二级域名,如 hello.qiniudn.com,也绑定了自定义域名(需要备案),如 hello.com。那么该 bucket 中 key 为 a/b/c.htm 的文件可以通过 http://hello.qiniudn.com/a/b/c.htm 或 http://hello.com/a/b/c.htm 中任意一个 url 进行访问。 -例如:执行 `rs.publish("cdn.example.com", function(resp){…})` 后,那么键名为 `foo/bar/file` 的文件可以通过 `http://cdn.example.com/foo/bar/file` 公开访问。 + -**参数** +##### 下载私有文件 -domain -: 必须,字符串类型(String),资源表发布的目标域名,例如:`cdn.example.com` +如果某个 bucket 是私有的,那么这个 bucket 中的所有文件只能通过一个的临时有效的 downloadUrl 访问: -callback function -: 请求完成之后执行的回调函数 + http:///?e=&token= -**响应** +其中 dntoken 是由业务服务器签发的一个[临时下载授权凭证](http://docs.qiniu.com/api/get.html#download-token),deadline 是 dntoken 的有效期。dntoken不需要单独生成,SDK 提供了生成完整 downloadUrl 的方法(包含了 dntoken),示例代码如下: -如果操作成功,回调函数的 resp 参数返回如下一段 json 信息: +```{javascript} +function downloadUrl(domain, key) { + var baseUrl = qiniu.rs.makeBaseUrl(domain, key); + var policy = new qiniu.rs.GetPolicy(); + return policy.makeRequest(baseUrl); +} +``` - { code: 200 } +生成 downloadUrl 后,服务端下发 downloadUrl 给客户端。客户端收到 downloadUrl 后,和公有资源类似,直接用任意的 HTTP 客户端就可以下载该资源了。唯一需要注意的是,在 downloadUrl 失效却还没有完成下载时,需要重新向服务器申请授权。 - +无论公有资源还是私有资源,下载过程中客户端并不需要七牛 SDK 参与其中。 -### 取消公开外链 + -取消指定 `bucket` 的在某个 `domain` 域下的所有公开外链访问。 +##### 断点续下载 - rs.unpublish(domain, function(resp) { - console.log("\n===> unpublish result: ", resp); - if (resp.code != 200) { - return; - } - }); +无论是公有资源还是私有资源,获得的下载 url 支持标准的 HTTP 断点续传协议。考虑到多数语言都有相应的断点续下载支持的成熟方法,七牛 Nodejs-SDK 并不提供断点续下载相关代码。 -参数 和 响应的返回值 同 `rs.publish()` 规格一致。 + - +### 资源操作 -### 删除指定文件 +资源操作限在服务端操作,先进行初始化 -`rs.remove()` 函数提供了从即定的 `bucket` 中删除指定的 `key`,即删除 `key` 索引关联的具体文件。 +```{javascript} +qiniu.conf.ACCESS_KEY = ''; +qiniu.conf.SECRET_KEY = ''; +``` - rs.remove(key, function(resp) { - console.log("\n===> remove result: ", resp); - if (resp.code != 200) { - return; - } - }); + -**响应** +#### 获取文件信息 -如果操作成功,回调函数的 resp 参数返回如下一段 json 信息: + - { code: 200 } +```{javascript} +var client = new qiniu.rs.Client(); +client.stat(bucketName, key, function(err, ret) { + if (!err) { + // ok + // ret has keys (hash, fsize, putTime, mimeType) + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +``` - + -### 删除所有文件(指定 bucket) +#### 删除文件 -`rs.drop()` 提供了删除整个 `bucket` 及其里边的所有 `key`,以及这些 `key` 关联的所有文件都将被删除。 +```{javascript} +var client = new qiniu.rs.Client(); +client.remove(bucketName, key, function(err, ret) { + if (!err) { + // ok + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}) +``` - rs.drop(function(resp) { - console.log("\n===> drop result: ", resp); - if (resp.code != 200) { - return; - } - }); + -**响应** +#### 复制/移动文件 -如果操作成功,回调函数的 resp 参数返回如下一段 json 信息: +```{javascript} +var client = new qiniu.rs.Client(); +client.copy(bucketSrc, keySrc, bucketDest, keyDest, function(err, ret) { + if (!err) { + // ok + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +``` - { code: 200 } +```{javascript} +var client = new qiniu.rs.Client(); +client.move(bucketSrc, keySrc, bucketDest, keyDest, function(err, ret) { + if (!err) { + // ok + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +``` + - - -### 图像处理 - - - -### 图像处理(缩略、裁剪、旋转、转化) - -`qiniu.img.mogrify()` 方法支持将一个存储在七牛云存储的图片进行缩略、裁剪、旋转和格式转化处理,该方法返回一个可以直接预览缩略图的URL。 - - var imgMogrPreviewURL = qiniu.img.mogrify(imageDownloadURL, options); - -**参数** - -imageDownloadURL -: 必须,字符串类型(string),指定原始图片的下载链接,可以根据 rs.get() 获取到。 - -options -: 必须,对象型(object),JSON 格式的图像处理参数。 - -`options` 对象具体的规格如下: +#### 批量操作 + +当您需要一次性进行多个操作时, 可以使用批量操作。 +批量操作的几个函数中,当返回值`err`为`null`的时候,有两种情况: + +1. 批量操作的所有操作都成功 +2. 批量操作仅仅部分成功 + +因此在返回成功的时候还需要检查各个子项目的返回值(详细见各个示例)。 +注意:批量操作不是对单个文件操作的包装,而是有独立的接口。 + +#### 批量获取文件信息 + +```{javascript} +var path0 = new qiniu.rs.EntryPath(bucketName, key0); +var path1 = new qiniu.rs.EntryPath(bucketName, key1); +var path2 = new qiniu.rs.EntryPath(bucketName, key2); +var client = new qiniu.rs.Client(); + +client.batchStat([path0, path1, path2], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i].code === 200) { + //ok, ret[i].data has keys (hash, fsize, putTime, mimeType) + } else { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } + } + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +``` + +#### 批量复制文件 + +```{javascript} +var pathSrc0 = new qiniu.rs.EntryPath(bucketName, key0); +var pathDest0 = new qiniu.rs.EntryPath(bucketName, key1); +var pathSrc1 = new qiniu.rs.EntryPath(bucketName, key2); +var pathDest1 = new qiniu.rs.EntryPath(bucketName, key3); + +var pair0 = new qiniu.rs.EntryPathPair(pathSrc0, pathDest0); +var pair1 = new qiniu.rs.EntryPathPair(pathSrc1, pathDest1); + +var client = new qiniu.rs.Client(); + +client.batchCopy([pair0, pair1], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i].code !== 200) { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } + } - options = { - "thumbnail": , - "gravity": , =NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast - "crop": , - "quality": , - "rotate": , - "format": , =jpg, gif, png, tif, etc. - "auto_orient": + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +``` + +#### 批量移动文件 + +```{javascript} +var pathSrc0 = new qiniu.rs.EntryPath(bucketName, key0); +var pathDest0 = new qiniu.rs.EntryPath(bucketName, key1); +var pathSrc1 = new qiniu.rs.EntryPath(bucketName, key2); +var pathDest1 = new qiniu.rs.EntryPath(bucketName, key3); + +var pair0 = new qiniu.rs.EntryPathPair(pathSrc0, pathDest0); +var pair1 = new qiniu.rs.EntryPathPair(pathSrc1, pathDest1); + +var client = new qiniu.rs.Client(); + +client.batchMove([pair0, pair1], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i] !== 200) { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } + } + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +``` + +#### 批量删除文件 + +```{javascript} +var path0 = new qiniu.rs.EntryPath(bucketName, key0); +var path1 = new qiniu.rs.EntryPath(bucketName, key1); +var path2 = new qiniu.rs.EntryPath(bucketName, key2); + +var client = new qiniu.rs.Client(); + +client.batchDelete([path0, path1, path2], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i].code !== 200) { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } } + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +``` -`qiniu.img.mogrify()` 方法是对七牛云存储图像处理高级接口的完整包装,关于 `options` 参数里边的具体含义和使用方式,可以参考文档:[图像处理高级接口](#/v3/api/foimg/#fo-imageMogr)。 + - +### 高级管理操作 -### 图像处理(缩略、裁剪、旋转、转化)并持久化存储处理结果 + +#### 列出文件 +请求某个存储空间(bucket)下的文件列表,如果有前缀,可以按前缀(prefix)进行过滤;第一次调用时置marker为null,之后的调用填上服务器返回的marker(如果有),则列出刚刚为列完的文件 -`qiniu.rs` 模块提供的 `imageMogrifyAs()` 方法支持将一个存储在七牛云存储的图片进行缩略、裁剪、旋转和格式转化处理,并且将处理后的缩略图作为一个新文件持久化存储到七牛云存储服务器上,这样就可以供后续直接使用而不用每次都传入参数进行图像处理。 +```{javascript} +qiniu.conf.ACCESS_KEY = ''; +qiniu.conf.SECRET_KEY = ''; - var conn = new qiniu.digestauth.Client(); +qiniu.rsf.listPrefix(bucketname, prefix, marker, limit, function(err, ret) { + if (!err) { + // process ret.marker & ret.items + } else { + console.log(err) + // http://docs.qiniu.com/api/file-handle.html#list + } +}); +``` - var imgrs = new qiniu.rs.Service(conn, thumbnails_bucket); + - imgrs.imageMogrifyAs(key, SourceImageDownloadURL, options, function(resp) { - console.log("\n===> imageMogrifyAs result: ", resp); - if (resp.code != 200) { - return; - } - }); +### 云处理 -**参数** + +#### 查看图像信息 -imageDownloadURL -: 必须,字符串类型(string),指定原始图片的下载链接,可以根据 rs.get() 获取到。 +```{javascript} +// 生成访问图片的url +var url = qiniu.rs.makeBaseUrl(bucketName, key); -options -: 必须,对象型(object),JSON 格式的图像处理参数。 +// 生成fop_url +var ii = new qiniu.fop.ImageInfo(); +url = ii.makeRequest(url); -`options` 对象具体的规格如下: +// 签名,生成private_url。如果是公有bucket则此步可以省略 +// 服务端操作使用,或者发送给客户端 +var policy = new qiniu.rs.GetPolicy(); +url = policy.makeRequest(url); - options = { - "thumbnail": , - "gravity": , =NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast - "crop": , - "quality": , - "rotate": , - "format": , = jpg, gif, png, tif, etc. - "auto_orient": - } +console.log('在浏览器输入: ' + url); +``` -`imgrs.imageMogrifyAs()` 方法同样是对七牛云存储图像处理高级接口的完整包装,关于 `options` 参数里边的具体含义和使用方式,可以参考文档:[图像处理高级接口](#/v3/api/foimg/#fo-imageMogr)。 +#### 查看图像Exif -**注意** +```{javascript} +// 生成访问图片的url +var url = qiniu.rs.makeBaseUrl(bucketName, key); -在上述示例代码中,我们实例化了一个新的 `imgrs` 对象,之所以这么做是因为我们考虑到缩略图也许可以创建公开外链,即缩略图所存放的 `thumbnails_bucket` 可以通过调用 `imgrs.publish()` 方法公开从而提供静态链接直接访问,这样做的好处是限定了作用域仅限于 `thumbnails_bucket`,也使得缩略图不必通过API通道进行请求且使用静态CDN加速访问,同时也保证了原图不受任何操作影响。 +// 生成fop_url +var exif = new qiniu.fop.Exif(); +url = exif.makeRequest(url); -为了使得调用 `imgrs.imageMogrifyAs()` 方法有实际意义,客户方的业务服务器必须保存 `` 和 `imgrs.imageMogrifyAs` 方法中参数 `` 的值。如此,该缩略图作为一个新文件可以使用 Node.js SDK 提供的任何方法。 +// 签名,生成private_url。如果是公有bucket则此步可以省略 +// 服务端操作使用,或者发送给客户端 +var policy = new qiniu.rs.GetPolicy(); +url = policy.makeRequest(url); -callback function -: 请求完成之后执行的回调函数 +console.log('在浏览器输入: ' + url); +``` -**响应** +#### 生成缩略图 -如果操作成功,回调函数的 resp 参数返回如下一段 json 信息: +```{javascript} +// 生成访问图片的url +var url = qiniu.rs.makeBaseUrl(bucketName, key); - { - code: 200, - data: { - hash: 'FrOXNat8VhBVmcMF3uGrILpTu8Cs' - } - } +// 生成fop_url +var iv = new qiniu.fop.ImageView(); +iv.width = 100; +url = iv.makeRequest(url); +// 签名,生成private_url。如果是公有bucket则此步可以省略 +// 服务端操作使用,或者发送给客户端 +var policy = new qiniu.rs.GetPolicy(); +url = policy.makeRequest(url); - +console.log('在浏览器输入: ' + url); +``` ## 贡献代码 -七牛云存储 Node.js SDK 开放源代码地址:[https://github.com/qiniu/nodejs-sdk](https://github.com/qiniu/nodejs-sdk) - -1. 登录 [github.com](https://github.com) -2. Fork [https://github.com/qiniu/nodejs-sdk](https://github.com/qiniu/nodejs-sdk) -3. 创建您的特性分支 (`git checkout -b my-new-feature`) -4. 提交您的改动 (`git commit -am 'Added some feature'`) -5. 将您的改动记录提交到远程 `git` 仓库 (`git push origin my-new-feature`) -6. 然后到 github 网站的该 `git` 远程仓库的 `my-new-feature` 分支下发起 Pull Request - - ++ Fork ++ 创建您的特性分支 (git checkout -b my-new-feature) ++ 提交您的改动 (git commit -am 'Added some feature') ++ 将您的修改记录提交到远程 git 仓库 (git push origin my-new-feature) ++ 然后到 github 网站的该 git 远程仓库的 my-new-feature 分支下发起 Pull Request ## 许可证 -Copyright (c) 2012 qiniu.com +> Copyright (c) 2013 qiniu.com -基于 MIT 协议发布: +## 基于 MIT 协议发布: -* [www.opensource.org/licenses/MIT](http://www.opensource.org/licenses/MIT) +> [www.opensource.org/licenses/MIT](http://www.opensource.org/licenses/MIT) diff --git a/docs/gist.py b/docs/gist.py new file mode 100755 index 00000000..939a779a --- /dev/null +++ b/docs/gist.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os +import re + +re_md_gist = re.compile(r"@gist\(([^\)]+)\)") +re_strip = re.compile(r"^\n*(.*)\n\t*$", re.S) +re_indent = re.compile(r"^(\t*)[^\t\s\n\r]") +line_start = r"(?:^|\n)\s*" +re_gist_comment = dict( + c = dict( + start = re.compile(r"%s\/\*\s*@gist\s+([\w\-_]+)\s*\*/.*?\n+" % line_start), + end = re.compile(r"%s\/\*\s*@endgist\s*\*/" % line_start), + ), + bash = dict( + start = re.compile(r"%s#\s*@gist\s+([\w\-_]+).*?\n+" % line_start), + end = re.compile(r"%s#\s*@endgist" % line_start), + ), + cpp = dict( + start = re.compile(r"%s//\s*@gist\s+([\w\-_]+).*?\n+" % line_start), + end = re.compile(r"%s//\s*@endgist" % line_start) + ), + html = dict( + start = re.compile(r"%s.*?\n+" % line_start), + end = re.compile(r"%s" % line_start), + ), +) +cpath = sys.path[0] + +def openfile(path): + if not os.path.exists(path): + return None + f = open(path, "r") + body = f.read() + f.close() + return body + +def get_gist_block(path): + gists = dict() + body = openfile(path) + if body is None: + return gists + start = 0 + while True: + a = search_one_block(body[start:]) + if a is None: + break + name, content, new_start = a + start += new_start + if not name in gists: + gists[name] = content + else: + gists[name].extend(["", "...", ""]) + gists[name].extend(content) + gists[""] = body.split("\n") + return gists + +def search_one_block(body): + if len(body) == 0: + return None + for n, regs in re_gist_comment.iteritems(): + a = regs["start"].search(body) + if a is None: + continue + start = a.span()[1] + b = regs["end"].search(body[start:]) + if b is None: + continue + break + if a is None or b is None: + return None + + body = body[start: b.span()[0]+start] + body = re_strip.sub("\\1", body) + start_indent = len(re_indent.findall(body)[0]) + body = [i[start_indent:] for i in body.split("\n")] + return a.group(1), body, b.span()[1] + start + +def dirname(path): + name = os.path.dirname(path) + if name == "": + name = "." + return name + +if __name__ == "__main__": + if len(sys.argv) <= 1: + sys.stderr.write("Usage: %s GistFile > OutputFile\n" % os.path.basename(sys.argv[0])) + exit(2) + + body = openfile(sys.argv[1]) + if body is None: + sys.stderr.write("Not such File.") + exit(2) + + rpath = dirname(sys.argv[1]) + body_gist_ref = [] + ref_files = [] + for i in re_md_gist.findall(body): + file_path = i + if i.find("#") > 0: + file_path = file_path.split("#")[0] + ref_files.append("%s/%s" % (rpath, file_path)) + body_gist_ref.append(i) + ref_files = list(set(ref_files)) + + match_gists = {} + for f in ref_files: + blocks = get_gist_block(f) + for block_key in blocks: + key = "%s#%s" % (f, block_key) + if len(block_key) == 0: + key = "%s%s" % (f, block_key) + match_gists[key] = blocks[block_key] + + errors = [] + for i in body_gist_ref: + key = "%s/%s" % (rpath, i) + if key in match_gists: + match_results = re_md_gist.search(body) + if match_results is None: + continue + s = match_results.span()[0] + s = body[body[s-50: s].rfind("\n")+s-50+1: s] + content = (("\n%s" % s).join(match_gists[key])).strip() + content = content.replace("\\", "\\\\") + + body = re.sub(r"@gist\s*\(%s\)" % i, content, body) + else: + errors.append(i) + + if len(errors) > 0: + sys.stderr.write("error: No Such File or Anchor\n") + for i, error in enumerate(errors): + sys.stderr.write("%s: '%s'\n" % (i+1, error)) + exit(2) + print body diff --git a/docs/gist/client.js b/docs/gist/client.js new file mode 100644 index 00000000..c9e9e220 --- /dev/null +++ b/docs/gist/client.js @@ -0,0 +1,45 @@ +var qiniu = require('../../'); + +// @gist uploadFile +function uploadFile(localFile, key, uptoken) { + var extra = new qiniu.io.PutExtra(); + //extra.params = params; + //extra.mimeType = mimeType; + //extra.crc32 = crc32; + //extra.checkCrc = checkCrc; + + qiniu.io.putFile(uptoken, key, localFile, extra, function(err, ret) { + if(!err) { + // 上传成功, 处理返回值 + console.log(ret.key, ret.hash); + // ret.key & ret.hash + } else { + // 上传失败, 处理返回代码 + console.log(err); + // http://docs.qiniu.com/api/put.html#error-code + } + }); +} +// @endgist + +// @gist uploadBuf +function uploadBuf(body, key, uptoken) { + var extra = new qiniu.io.PutExtra(); + //extra.params = params; + //extra.mimeType = mimeType; + //extra.crc32 = crc32; + //extra.checkCrc = checkCrc; + + qiniu.io.put(uptoken, key, body, extra, function(err, ret) { + if (!err) { + // 上传成功, 处理返回值 + console.log(ret.key, ret.hash); + // ret.key & ret.hash + } else { + // 上传失败, 处理返回代码 + console.log(err) + // http://docs.qiniu.com/api/put.html#error-code + } + }); +} +// @endgist diff --git a/docs/gist/fop.js b/docs/gist/fop.js new file mode 100644 index 00000000..eb798ac4 --- /dev/null +++ b/docs/gist/fop.js @@ -0,0 +1,56 @@ +var qiniu = require('../../'); + +qiniu.conf.ACCESS_KEY = '8Y7uZY0cqHxAyGK27V_B2Bxf8IhAkqEPOHr6iwwc'; +qiniu.conf.SECRET_KEY = '1uvFVvk9IqFRQ6t4TCr-DdeXybTbSS0gauJrYiJN'; + +var bucketName = 'test369.qiniudn.com'; +var key = 'logo.png'; +// @gist makeImageInfoUrl +// 生成访问图片的url +var url = qiniu.rs.makeBaseUrl(bucketName, key); + +// 生成fop_url +var ii = new qiniu.fop.ImageInfo(); +url = ii.makeRequest(url); + +// 签名,生成private_url。如果是公有bucket则此步可以省略 +// 服务端操作使用,或者发送给客户端 +var policy = new qiniu.rs.GetPolicy(); +url = policy.makeRequest(url); + +console.log('在浏览器输入: ' + url); +// @endgist + +// @gist makeExifUrl +// 生成访问图片的url +var url = qiniu.rs.makeBaseUrl(bucketName, key); + +// 生成fop_url +var exif = new qiniu.fop.Exif(); +url = exif.makeRequest(url); + +// 签名,生成private_url。如果是公有bucket则此步可以省略 +// 服务端操作使用,或者发送给客户端 +var policy = new qiniu.rs.GetPolicy(); +url = policy.makeRequest(url); + +console.log('在浏览器输入: ' + url); +// @endgist + +// @gist makeImageViewUrl + +// 生成访问图片的url +var url = qiniu.rs.makeBaseUrl(bucketName, key); + +// 生成fop_url +var iv = new qiniu.fop.ImageView(); +iv.width = 100; +url = iv.makeRequest(url); + +// 签名,生成private_url。如果是公有bucket则此步可以省略 +// 服务端操作使用,或者发送给客户端 +var policy = new qiniu.rs.GetPolicy(); +url = policy.makeRequest(url); + +console.log('在浏览器输入: ' + url); +// @endgist diff --git a/docs/gist/rs.js b/docs/gist/rs.js new file mode 100644 index 00000000..99ef52dc --- /dev/null +++ b/docs/gist/rs.js @@ -0,0 +1,159 @@ +var qiniu = require('../../'); + +// @gist init +qiniu.conf.ACCESS_KEY = ''; +qiniu.conf.SECRET_KEY = ''; +// @endgist + + +// @gist stat +var client = new qiniu.rs.Client(); +client.stat(bucketName, key, function(err, ret) { + if (!err) { + // ok + // ret has keys (hash, fsize, putTime, mimeType) + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +// @endgist + +// @gist move +var client = new qiniu.rs.Client(); +client.move(bucketSrc, keySrc, bucketDest, keyDest, function(err, ret) { + if (!err) { + // ok + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +// @endgist + +// @gist copy +var client = new qiniu.rs.Client(); +client.copy(bucketSrc, keySrc, bucketDest, keyDest, function(err, ret) { + if (!err) { + // ok + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +// @endgist + +// @gist remove +var client = new qiniu.rs.Client(); +client.remove(bucketName, key, function(err, ret) { + if (!err) { + // ok + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}) +// @endgist + +// @gist batchStat +var path0 = new qiniu.rs.EntryPath(bucketName, key0); +var path1 = new qiniu.rs.EntryPath(bucketName, key1); +var path2 = new qiniu.rs.EntryPath(bucketName, key2); +var client = new qiniu.rs.Client(); + +client.batchStat([path0, path1, path2], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i].code === 200) { + //ok, ret[i].data has keys (hash, fsize, putTime, mimeType) + } else { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } + } + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +// @endgist + +// @gist batchCopy +var pathSrc0 = new qiniu.rs.EntryPath(bucketName, key0); +var pathDest0 = new qiniu.rs.EntryPath(bucketName, key1); +var pathSrc1 = new qiniu.rs.EntryPath(bucketName, key2); +var pathDest1 = new qiniu.rs.EntryPath(bucketName, key3); + +var pair0 = new qiniu.rs.EntryPathPair(pathSrc0, pathDest0); +var pair1 = new qiniu.rs.EntryPathPair(pathSrc1, pathDest1); + +var client = new qiniu.rs.Client(); + +client.batchCopy([pair0, pair1], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i].code !== 200) { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } + } + + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +// @endgist + + +// @gist batchMove +var pathSrc0 = new qiniu.rs.EntryPath(bucketName, key0); +var pathDest0 = new qiniu.rs.EntryPath(bucketName, key1); +var pathSrc1 = new qiniu.rs.EntryPath(bucketName, key2); +var pathDest1 = new qiniu.rs.EntryPath(bucketName, key3); + +var pair0 = new qiniu.rs.EntryPathPair(pathSrc0, pathDest0); +var pair1 = new qiniu.rs.EntryPathPair(pathSrc1, pathDest1); + +var client = new qiniu.rs.Client(); + +client.batchMove([pair0, pair1], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i] !== 200) { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } + } + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +// @endgist + +// @gist batchDelete +var path0 = new qiniu.rs.EntryPath(bucketName, key0); +var path1 = new qiniu.rs.EntryPath(bucketName, key1); +var path2 = new qiniu.rs.EntryPath(bucketName, key2); + +var client = new qiniu.rs.Client(); + +client.batchDelete([path0, path1, path2], function(err, ret) { + if (!err) { + for (i in ret) { + if (ret[i].code !== 200) { + // parse error code + console.log(ret[i].code, ret[i].data); + // http://docs.qiniu.com/api/file-handle.html#error-code + } + } + } else { + console.log(err); + // http://docs.qiniu.com/api/file-handle.html#error-code + } +}); +// @endgist diff --git a/docs/gist/rsf.js b/docs/gist/rsf.js new file mode 100644 index 00000000..5ec707e0 --- /dev/null +++ b/docs/gist/rsf.js @@ -0,0 +1,18 @@ +var qiniu = require('../../'); + +// @gist listPrefix +qiniu.conf.ACCESS_KEY = ''; +qiniu.conf.SECRET_KEY = ''; + +qiniu.rsf.listPrefix(bucketname, prefix, marker, limit, function(err, ret) { + if (!err) { + // process ret.marker & ret.items + } else { + console.log(err) + // http://docs.qiniu.com/api/file-handle.html#list + } +}); +// @endgist + + + diff --git a/docs/gist/server.js b/docs/gist/server.js new file mode 100644 index 00000000..02905930 --- /dev/null +++ b/docs/gist/server.js @@ -0,0 +1,28 @@ +var qiniu = require('../../'); + +// @gist init +qiniu.conf.ACCESS_KEY = '' +qiniu.conf.SECRET_KEY = '' +// @endgist + +// @gist uptoken +function uptoken(bucketname) { + var putPolicy = new qiniu.rs.PutPolicy(bucketname); + //putPolicy.callbackUrl = callbackUrl; + //putPolicy.callbackBody = callbackBody; + //putPolicy.returnUrl = returnUrl; + //putPolicy.returnBody = returnBody; + //putPolicy.asyncOps = asyncOps; + //putPolicy.expires = expires; + + return putPolicy.token(); +} +// @endgist + +// @gist downloadUrl +function downloadUrl(domain, key) { + var baseUrl = qiniu.rs.makeBaseUrl(domain, key); + var policy = new qiniu.rs.GetPolicy(); + return policy.makeRequest(baseUrl); +} +// @endgist diff --git a/index.js b/index.js index b5c2c61c..6b224d3b 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,10 @@ -var libpath = process.env.QINIU_COV ? './lib-cov' : './lib'; + +var libpath = process.env.QINIU_COV ? './lib-cov' : './qiniu'; module.exports = { - conf: require(libpath + '/conf.js'), - digestauth: require(libpath + '/digestauth.js'), - rs: require(libpath + '/rs.js'), - img: require(libpath + '/img.js'), - auth: require(libpath + '/auth.js'), + io: require(libpath + '/io.js'), + rs: require(libpath + '/rs.js'), + rsf: require(libpath + '/rsf.js'), + fop: require(libpath + '/fop.js'), + conf: require(libpath + '/conf.js'), }; diff --git a/lib/auth.js b/lib/auth.js deleted file mode 100644 index 7fa9cdde..00000000 --- a/lib/auth.js +++ /dev/null @@ -1,94 +0,0 @@ -var crypto = require('crypto'); -var config = require("./conf.js"); -var util = require('./util.js'); - -exports.PutPolicy = PutPolicy; -exports.GetPolicy = GetPolicy; - -// ------------------------------------------------------------------------------------------ -// func generateSignature - -function generateSignature(params) { - var paramsString = JSON.stringify(params); - return util.encode(paramsString); -} - -// ------------------------------------------------------------------------------------------ -// func generateEncodedDigest - -function generateEncodedDigest(signature) { - var hmac = crypto.createHmac('sha1', config.SECRET_KEY); - hmac.update(signature); - var digest = hmac.digest('base64'); - return util.base64ToUrlsafe(digest); -} - -// ------------------------------------------------------------------------------------------ -// func generateToken - -function generateToken(params) { - var signature = generateSignature(params); - var encodedDigest = generateEncodedDigest(signature); - return config.ACCESS_KEY + ":" + encodedDigest + ":" + signature; -} - -// ------------------------------------------------------------------------------------------ -// type PutPolicy - -function PutPolicy(opts) { - this.scope = opts.scope || null; - this.expires = opts.expires || 3600; - this.callbackUrl = opts.callbackUrl || null; - this.callbackBodyType = opts.callbackBodyType || null; - this.customer = opts.customer || null; - this.escape = opts.escape || 0; - this.asyncOps = opts.asyncOps || null; - this.returnBody = opts.returnBody || null; -} - -PutPolicy.prototype.token = function() { - var params = { - "deadline": this.expires + Math.floor(Date.now() / 1000) - }; - if (this.scope !== null) { - params["scope"] = this.scope; - } - if (this.callbackUrl !== null) { - params["callbackurl"] = this.callbackUrl; - } - if (this.callbackBodyType !== null) { - params["callbackBodyType"] = this.callbackBodyType; - } - if (this.customer !== null) { - params["customer"] = this.customer; - } - if (this.asyncOps !== null) { - params["asyncOps"] = this.asyncOps; - } - if (this.escape) { - params["escape"] = this.excape; - } - if (this.returnBody !== null) { - params["returnBody"] = this.returnBody; - } - return generateToken(params); -}; - -// ------------------------------------------------------------------------------------------ -// type GetPolicy - -function GetPolicy(opts) { - this.expires = opts.expires || 3600; - this.scope = opts.scope; // GetPolicy.scope 没有默认值:用 "*/*" 访问权限太高! -} - -GetPolicy.prototype.token = function() { - var params = { - S: this.scope, - E: this.expires + Math.floor(Date.now() / 1000), - }; - return generateToken(params); -}; - -// ------------------------------------------------------------------------------------------ - diff --git a/lib/conf.js b/lib/conf.js deleted file mode 100644 index b4894610..00000000 --- a/lib/conf.js +++ /dev/null @@ -1,20 +0,0 @@ -// ------------------------------------------------------------------------------------------ -exports.ACCESS_KEY = ''; -exports.SECRET_KEY = ''; - -exports.REDIRECT_URI = ''; -exports.AUTHORIZATION_ENDPOINT = ''; -exports.TOKEN_ENDPOINT = 'https://acc.qbox.me/oauth2/token'; - -exports.PUT_TIMEOUT = 300000; // 300s = 5m -exports.BLOCK_SIZE = 1024*1024*4; // Block Size: 4MB -exports.CHUNK_SIZE = 1024*256; // Chunk Size: 256KB -exports.MAX_RETRY_TIMES = 3; // Max retry times: 3 - -exports.IO_HOST = 'http://iovip.qbox.me'; -//exports.FS_HOST = 'https://fs.qbox.me'; -exports.RS_HOST = 'http://rs.qbox.me'; -exports.UP_HOST = 'http://up.qbox.me'; - -// ------------------------------------------------------------------------------------------ - diff --git a/lib/digestauth.js b/lib/digestauth.js deleted file mode 100644 index 40a85bc7..00000000 --- a/lib/digestauth.js +++ /dev/null @@ -1,148 +0,0 @@ -var crypto = require('crypto'); -var http = require('http'); -var https = require('https'); -var uri = require('url'); -var querystring = require('querystring'); -var conf = require('./conf.js'); -var util = require('./util.js'); - -// ------------------------------------------------------------------------------------------ -// func checksum - -function checksum(opt, body) { - var hmac = crypto.createHmac('sha1', conf.SECRET_KEY); - hmac.update(opt.path + "\n"); - if (body) { - hmac.update(body); - } - var digest = hmac.digest('base64'); - return util.base64ToUrlsafe(digest); -} - -// ------------------------------------------------------------------------------------------ -// type Client - -function Client() { -} - -Client.prototype.execute = function(options, url, params, onresp, onerror) { - var u = uri.parse(url); - var opt = { - headers: {'Accept': 'application/json', 'Accept-Encoding': 'gzip, deflate'}, - host: u.hostname, - port: u.port, - path: u.path, - method: 'POST' - }; - - var proto; - if (u.protocol === 'https:') { - proto = https; - } else { - proto = http; - } - - var body; - var isStream = false; - var isText = true; - var contentLength = 0; - var contentType = 'application/x-www-form-urlencoded'; - if (params) { - if (params instanceof util.Binary) { - contentType = 'application/octet-stream'; - contentLength = params.bytes; - isStream = true; - } else if (params instanceof util.Form) { - contentType = params.contentType; - contentLength = null; - isStream = true; - } else { - if (typeof params === 'string') { - body = params; - } else { - body = querystring.stringify(params); - } - contentLength = body.length; - } - } - - opt.headers['Content-Type'] = contentType; - if (contentLength !== null) { - opt.headers['Content-Length'] = contentLength; - } - - if (options.UploadSignatureToken != undefined && options.UploadSignatureToken != null && options.UploadSignatureToken != "") { - opt.headers['Authorization'] = 'UpToken ' + options.UploadSignatureToken; - } else if (options.AccessToken != undefined && options.AccessToken != null && options.AccessToken != "") { - opt.headers['Authorization'] = 'Bearer ' + options.AccessToken; - } else { - opt.headers['Authorization'] = 'QBox ' + conf.ACCESS_KEY + ':' + checksum(opt, body); - } - - var req = proto.request(opt, onresp); - req.on('error', onerror); - - if (params) { - if (isStream) { - params.stream.pipe(req); - } else { - req.end(params); - } - } else { - req.end(); - } - return req; -}; - -Client.prototype._callWith = function(options, url, params, onret) { - - var onresp = function(res) { - util.readAll(res, function(data) { - var ret; - if (data.length === 0) { - ret = {code: res.statusCode}; - if (res.statusCode !== 200) { - ret.error = 'E' + res.statusCode; - } - onret(ret); - return; - } - try { - ret = JSON.parse(data); - if (res.statusCode === 200) { - ret = {code: 200, data: ret}; - } else { - ret.code = res.statusCode; - } - } catch (e) { - ret = {code: -2, error: e.toString(), detail: e}; - } - onret(ret); - }); - }; - - var onerror = function(e) { - var ret = { - code: -1, - error: e.message, - detail: e - }; - onret(ret); - }; - - return this.execute(options, url, params, onresp, onerror); -}; - -Client.prototype.callWith = function(url, params, onret) { - return this._callWith("", url, params, onret); -} - -Client.prototype.callWithToken = function(uploadToken, url, params, onret){ - var options = { 'UploadSignatureToken': uploadToken }; - return this._callWith(options, url, params, onret); -}; - -exports.Client = Client; - -// ------------------------------------------------------------------------------------------ - diff --git a/lib/img.js b/lib/img.js deleted file mode 100644 index e0dd31ce..00000000 --- a/lib/img.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 图像处理接口,生成图像处理的参数 - * func mkMogrifyParams() => string - * opts = { - * "thumbnail": , - * "gravity": , =NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast - * "crop": , - * "quality": , - * "rotate": , - * "format": , =jpg, gif, png, tif, etc. - * "auto_orient": - * } - */ -exports.mkMogrifyParams = function(opts){ - opts = opts || {}; - var keys = ["thumbnail", "gravity", "crop", "quality", "rotate", "format"]; - var params_string = "", key = null, val = null; - if (undefined !== opts.auto_orient && opts.auto_orient === true){ - params_string += "/auto-orient"; - } - for (var i=0; i < keys.length; i++) { - key = keys[i]; - if (undefined !== opts[key]) { - params_string += '/' + key + '/' + opts[key]; - } - } - return 'imageMogr' + params_string; -}; - -/* - * 图像处理接口,生成最终的缩略图预览URL - */ -exports.mogrify = function(source_img_url, opts){ - return source_img_url + '?' + this.mkMogrifyParams(opts); -}; diff --git a/lib/rs.js b/lib/rs.js deleted file mode 100644 index 3c306483..00000000 --- a/lib/rs.js +++ /dev/null @@ -1,410 +0,0 @@ -var fs = require('fs'); -var path = require('path'); -var crc32 = require('crc32'); -var mime = require('mime'); -var formstream = require('formstream'); -var querystring = require('querystring'); -var config = require('./conf.js'); -var util = require('./util.js'); -var img = require('./img.js'); - -exports.Service = Service; -exports.mkbucket = function(conn, bucketname, onret) { - var url = config.RS_HOST + '/mkbucket/' + bucketname; - conn.callWith(url, null, onret); -}; - -// ------------------------------------------------------------------------------------------ -// type Service - -function Service(conn, bucket) { - this.conn = conn; - this.bucket = bucket; -} - -Service.prototype.buckets = function(onret) { - var url = config.RS_HOST + '/buckets'; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.putAuth = function(onret) { - /* - * func PutAuth() => PutAuthRet - * 上传授权(生成一个短期有效的可匿名上传URL) - **/ - var url = config.IO_HOST + '/put-auth/'; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.putAuthEx = function(expires, callbackUrl, onret) { - /* - * func PutAuthEx(expires, callbackUrl) => PutAuthRet - * 上传授权(生成一个短期有效的可匿名上传URL) - **/ - var url = config.IO_HOST + '/put-auth/' + expires + '/callback/' + util.encode(callbackUrl); - this.conn.callWith(url, null, onret); -}; - -Service.prototype.put = function(key, mimeType, fp, bytes, onret) { - /* - * func Put(key string, mimeType string, fp File, bytes int64) => (data PutRet, code int, err Error) - * 上传一个流 - **/ - if (!mimeType) { - mimeType = 'application/octet-stream'; - } - var entryURI = this.bucket + ':' + key; - var url = config.IO_HOST + '/rs-put/' + util.encode(entryURI) + '/mimeType/' + util.encode(mimeType); - var binary = new util.Binary(fp, bytes); - return this.conn.callWith(url, binary, onret); -}; - -Service.prototype.putFile = function(key, mimeType, localFile, onret) { - /* - * func PutFile(key string, mimeType string, localFile string) => (data PutRet, code int, err Error) - * 上传文件 - **/ - var self = this; - if (!mimeType) { - mimeType = mime.lookup(localFile); - } - fs.stat(localFile, function(err, fi) { - if (err) { - onret({code: -1, error: err.toString(), detail: err}); - return; - } - var fp = fs.createReadStream(localFile); - self.put(key, mimeType, fp, fi.size, onret); - }); -}; - -Service.prototype.upload = function(upToken, key, mimeType, filename, stream, onret) { - /* - * func Upload(upToken string, key string, mimeType string, filename string, stream ReadStream) => (data PutRet, code int, err Error) - * 以multipart/form-data形式上传ReadStream - **/ - var self = this; - if (!mimeType) { - mimeType = "application/octet-stream"; - } - var entryURI = this.bucket + ':' + key; - entryURI = '/rs-put/' + util.encode(entryURI) + '/mimeType/' + util.encode(mimeType); - - var form = formstream(); - form.field('action', entryURI); - form.stream('file', stream, filename, mimeType); - - form = new util.Form(form, form.headers()['Content-Type']); - return this.conn.callWith(upToken, form, onret); -}; - -Service.prototype.uploadFile = function(upToken, key, mimeType, localFile, onret) { - /* - * func UploadFile(upToken string, key string, mimeType string, localFile string) => (data PutRet, code int, err Error) - * 以multipart/form-data形式上传文件 - **/ - var self = this; - if (!mimeType) { - mimeType = mime.lookup(localFile); - } - fs.stat(localFile, function(err, fi) { - if (err) { - onret({code: -1, error: err.toString(), detail: err}); - return; - } - var filename = path.basename(localFile); - var stream = fs.createReadStream(localFile); - self.upload(upToken, key, mimeType, filename, stream, onret); - }); -}; - -Service.prototype.uploadWithToken = function(uploadToken, stream, key, mimeType, customMeta, callbackParams, crc32, onret) { - /* - * func UploadWithToken(uploadToken, stream, key, mimeType, customMeta, callbackParams, crc32, onret) => (data PutRet, code int, err Error) - * 使用upload_token以multipart/form-data形式上传ReadStream流 - **/ - var bucket = this.bucket; - if (!mimeType) { - mimeType = "application/octet-stream"; - } - - var actionString = util.generateActionString(bucket, key, mimeType, customMeta, crc32); - if (callbackParams === null) { - callbackParams = { - "bucket": bucket, - "key": key, - "mime_type": mimeType - }; - } - var callbackQueryString = querystring.stringify(callbackParams); - var url = config.UP_HOST + "/upload"; - - var filename = path.basename(key); - var form = formstream(); - form.field('action', actionString); - form.field('params', callbackQueryString); - form.field('multipart', true); - form.field('auth', uploadToken); - form.stream('file', stream, filename, mimeType); - form = new util.Form(form, form.headers()['Content-Type']); - - return this.conn.callWithToken(uploadToken, url, form, onret); -}; - -Service.prototype.uploadFileWithToken = function(uploadToken, localFile, key, mimeType, customMeta, callbackParams, enableCrc32Check, onret) { - /* - * func UploadFileWithToken(uploadToken, localFile, key, mimeType, customMeta, callbackParams, enableCrc32Check, onret) => (data PutRet, code int, err Error) - * 使用upload_token以multipart/form-data形式上传文件 - **/ - var self = this - , bucket = self.bucket; - if (!mimeType) { - mimeType = mime.lookup(localFile); - } - fs.stat(localFile, function(err, fi) { - if (err) { - onret({code: -1, error: err.toString(), detail: err}); - return; - } - var fileCrc32 = null - , stream = fs.createReadStream(localFile); - - if (enableCrc32Check) { - var fileStat = fs.statSync(localFile) - , fileSize = fileStat.size - , buf = new Buffer(fileSize) - , fd = fs.openSync(localFile, 'r'); - - fs.readSync(fd, buf, 0, fileSize, 0); - fs.closeSync(fd); - fileCrc32 = parseInt("0x" + crc32(buf)).toString(); - } - - self.uploadWithToken(uploadToken, stream, key, mimeType, customMeta, callbackParams, fileCrc32, onret); - }); -}; - -Service.prototype.get = function(key, attName, onret) { - /* - * func Get(key string, attName string) => GetRet - * 下载授权(生成一个短期有效的可匿名下载URL) - **/ - var entryURI = this.bucket + ':' + key; - var url = config.RS_HOST + '/get/' + util.encode(entryURI) + '/attName/' + util.encode(attName); - this.conn.callWith(url, null, onret); -}; - -Service.prototype.getIfNotModified = function(key, attName, base, onret) { - /* - * func GetIfNotModified(key string, attName string, base string) => GetRet - * 下载授权(生成一个短期有效的可匿名下载URL),如果服务端文件没被人修改的话(用于断点续传) - **/ - var entryURI = this.bucket + ':' + key; - var url = config.RS_HOST + '/get/' + util.encode(entryURI) + '/attName/' + util.encode(attName) + '/base/' + base; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.stat = function(key, onret) { - /* - * func Stat(key string) => Entry - * 取资源属性 - */ - var entryURI = this.bucket + ':' + key; - var url = config.RS_HOST + '/stat/' + util.encode(entryURI); - this.conn.callWith(url, null, onret); -}; - -Service.prototype.publish = function(domain, onret) { - /* - * func Publish(domain string) => Bool - * 将本 Table 的内容作为静态资源发布。静态资源的url为:http://domain/key - **/ - var url = config.RS_HOST + '/publish/' + util.encode(domain) + '/from/' + this.bucket; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.unpublish = function(domain, onret) { - /* - * func Unpublish(domain string) => Bool - * 取消发布 - */ - var url = config.RS_HOST + '/unpublish/' + util.encode(domain); - this.conn.callWith(url, null, onret); -}; - -Service.prototype.remove = function(key, onret) { - /* - * func Delete(key string) => Bool - * 删除资源 - **/ - var entryURI = this.bucket + ':' + key; - var url = config.RS_HOST + '/delete/' + util.encode(entryURI); - this.conn.callWith(url, null, onret); -}; - -Service.prototype.drop = function(onret) { - /* - * func Drop() => Bool - * 删除整个表(慎用!) - **/ - var url = config.RS_HOST + '/drop/' + this.bucket; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.copy = function(sourceBucket, sourceKey, targetBucket, targetKey, onret) { - /* - * func Copy(sourceBucket, sourceKey, targetBucket, targetKey, onret) => Bool - * 拷贝某个资源表中的文件到另一个资源表中的某个文件 - */ - var url = config.RS_HOST + generateMoveOrCopyOpString('copy', sourceBucket, sourceKey, targetBucket, targetKey); - this.conn.callWith(url, null, onret); -}; - -Service.prototype.move = function(sourceBucket, sourceKey, targetBucket, targetKey, onret) { - /* - * func Move(sourceBucket, sourceKey, targetBucket, targetKey, onret) => Bool - * 移动某个资源表中的文件到另一个资源表中的某个文件 - */ - var url = config.RS_HOST + generateMoveOrCopyOpString('move', sourceBucket, sourceKey, targetBucket, targetKey); - this.conn.callWith(url, null, onret); -}; - -Service.prototype.batchGet = function(bucket, keys, onret) { - /* - * func BatchGet(bucket, keys, onret) => GetRet[] - * 为每个key生成一个短期有效的下载地址 - */ - batch(this, "get", bucket, keys, onret); -}; - -Service.prototype.batchStat = function(bucket, keys, onret) { - /* - * func BatchStat(bucket, keys, onret) => Entry[] - * 查看每个key所对应文件的属性 - */ - batch(this, "stat", bucket, keys, onret); -}; - -Service.prototype.batchDelete = function(bucket, keys, onret) { - /* - * func BatchDelete(bucket, keys, onret) => Bool[] - * 批量删除每个key所对应的资源 - */ - batch(this, "delete", bucket, keys, onret); -}; - -Service.prototype.batchCopy = function(entries, onret) { - /* - * func BatchCopy(entries, onret) => Bool[] - * 批量拷贝文件 - */ - batchMoveOrCopy(this, 'copy', entries, onret); -}; - -Service.prototype.batchMove = function(entries, onret) { - /* - * func BatchMove(entries, onret) => Bool[] - * 批量移动文件 - */ - batchMoveOrCopy(this, 'move', entries, onret); -}; - -/* - * 持久化存储一个经过云端服务处理过后的资源 - */ -Service.prototype.saveAs = function(key, source_url, opWithParams, onret) { - var destEntryURI = this.bucket + ':' + key; - var saveAsEntryURI = util.encode(destEntryURI); - var saveAsParam = "/save-as/" + saveAsEntryURI; - var newurl = source_url + '?' + opWithParams + saveAsParam; - this.conn.callWith(newurl, null, onret); -}; - -/* - * 图像处理接口(可持久化存储缩略图) - * func imageMogrifyAs(, , , ) => Entry - * opts = { - * "thumbnail": , - * "gravity": , =NorthWest, North, NorthEast, West, Center, East, SouthWest, South, SouthEast - * "crop": , - * "quality": , - * "rotate": , - * "format": , =jpg, gif, png, tif, etc. - * "auto_orient": - * } - */ -Service.prototype.imageMogrifyAs = function(key, source_img_url, opts, onret) { - var mogrifyParams = img.mkMogrifyParams(opts); - this.saveAs(key, source_img_url, mogrifyParams, onret); -}; - -/* - * 水印设置接口 - * setProtected() - 设置原图保护 - * setSeparator() - 设置分隔符 - * setStyle() - 设置图片预览风格别名 - * unsetStyle() - 取消设置图片预览风格别名 -*/ -Service.prototype.setProtected = function(protectedMode, onret){ - var url = config.PUB_HOST + "/accessMode/" + this.bucket + "/mode/" + protectedMode; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.setSeparator = function(sep, onret){ - sep = util.encode(sep); - var url = config.PUB_HOST + "/separator/" + this.bucket + "/sep/" + sep; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.setStyle = function(name, style, onret){ - name = util.encode(name); - style = util.encode(style); - var url = config.PUB_HOST + "/style/" + this.bucket + "/name/" + name + "/style/" + style; - this.conn.callWith(url, null, onret); -}; - -Service.prototype.unsetStyle = function(name, onret){ - name = util.encode(name); - var url = config.PUB_HOST + "/unstyle/" + this.bucket + "/name/" + name; - this.conn.callWith(url, null, onret); -}; - - -// ------------------------------------------------------------------------------------------ -// private functions - -function generateMoveOrCopyOpString(command, sourceBucket, sourceKey, targetBucket, targetKey) { - var sourceEntryURI = sourceBucket + ":" + sourceKey; - var targetEntryURI = targetBucket + ":" + targetKey; - var url = '/' + command + '/' + util.encode(sourceEntryURI) + '/' + util.encode(targetEntryURI); - return url; -}; - -function batch(rs, command, bucket, keys, onret) { - var ops = [] - , length = keys.length - , url = config.RS_HOST + '/batch?'; - - for(var i = 0; i < length; i++) { - console.log("Entry URI is: ", bucket + ":" + keys[i]); - var encodedEntryURI = util.encode(bucket + ":" + keys[i]); - ops.push("op=/" + command + "/" + encodedEntryURI); - } - url += ops.join("&"); - console.log("Batch URL: ", url); - rs.conn.callWith(url, null, onret); -} - -function batchMoveOrCopy(rs, command, entries, onret) { - var ops = [] - , length = ops.length - , url = config.RS_HOST + '/batch?'; - - for(var i = 0; i < length; i++) { - ops.push('op=' + moveOrCopy(command, entries[i][0], entries[i][1], entries[i][2], entries[i][3])); - } - url += ops.join("&"); - rs.conn.callWith(url, null, onret); -} - -// ------------------------------------------------------------------------------------------ diff --git a/lib/util.js b/lib/util.js deleted file mode 100644 index 387c149d..00000000 --- a/lib/util.js +++ /dev/null @@ -1,98 +0,0 @@ -var fs = require('fs'); -var path = require('path'); -var mime = require('mime'); -var crypto = require('crypto'); - -// ------------------------------------------------------------------------------------------ -// func encode - -exports.base64ToUrlsafe = function(v) { - return v.replace(/\//g, '_').replace(/\+/g, '-'); -}; - -exports.encode = function(v) { - var encoded = new Buffer(v || '').toString('base64'); - return exports.base64ToUrlsafe(encoded); -}; - -exports.generateActionString = function(bucket, key, mimeType, customMeta, crc32) { - if (!key) { - console.error("Please specify your key!"); - return; - } - var entryUri = bucket + ":" + key; - if (!mimeType) { - mimeType = "application/octet-stream"; - } - var actionParams = '/rs-put/' + this.encode(entryUri) + '/mimeType/' + this.encode(mimeType); - if (customMeta !== "") { - actionParams += '/meta/' + this.encode(customMeta); - } - if ((crc32 !== undefined) && (crc32 !== null) && (crc32 !== "")) { - actionParams += '/crc32/' + crc32; - } - return actionParams; -} - -// ------------------------------------------------------------------------------------------ -// func readAll - -exports.readAll = function(strm, ondata) { - var out = []; - var total = 0; - strm.on('data', function(chunk) { - out.push(chunk); - total += chunk.length; - }); - strm.on('end', function() { - var data; - switch (out.length) { - case 0: - data = new Buffer(0); - break; - case 1: - data = out[0]; - break; - default: - data = new Buffer(total); - var pos = 0; - for (var i = 0; i < out.length; i++) { - var chunk = out[i]; - chunk.copy(data, pos); - pos += chunk.length; - } - } - ondata(data); - }); -}; - -// ------------------------------------------------------------------------------------------ -// type Binary - -function Binary(stream, bytes) { - this.stream = stream; - this.bytes = bytes; -} - -exports.Binary = Binary; - -// type Form - -function Form(stream, contentType) { - this.stream = stream; - this.contentType = contentType; -} - -exports.Form = Form; - -// type Text - -function Text(text, contentType) { - this.text = text; - this.contentType = contentType; -} - -exports.Text = Text; - -// ------------------------------------------------------------------------------------------ - diff --git a/package.json b/package.json index 290ec119..956a03ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qiniu", - "version": "2.4.3", + "version": "6.0.0", "description": "Node wrapper for Qiniu Resource (Cloud) Storage API", "main": "index.js", "directories": { @@ -36,14 +36,18 @@ { "name": "ikbear", "email": "sunikbear@gmail.com" + }, + { + "name": "lintianzhi", + "email": "lintianzhi1992@gmail.com" } ], "engines": [ "node >= 0.4.7" ], "dependencies": { - "mime": "1.2.7", - "formstream": "0.0.2", + "mime": "1.2.9", + "formstream": "0.0.5", "crc32": "0.2.2" }, "devDependencies": { diff --git a/qiniu/auth/digest.js b/qiniu/auth/digest.js new file mode 100644 index 00000000..8db1bae2 --- /dev/null +++ b/qiniu/auth/digest.js @@ -0,0 +1,39 @@ + +var url = require('url'); +var conf = require('../conf'); +var util = require('../util'); + +exports.Mac = Mac; + +function Mac(accessKey, secretKey) { + this.accessKey = accessKey || conf.ACCESS_KEY; + this.secretKey = secretKey || conf.SECRET_KEY; +} + +//Mac.prototype._sign = function(data) { +// return util.hmacSha1(data, this.secretKey); +//} +// +//Mac.prototype.sign = function(data) { +// return this.accessKey + ':' + this._sign(data); +//} +// +//Mac.prototype.signWithData = function(b) { +// var data = util.urlsafeBase64Encode(b); +// var sign = this._sign(data); +// return this.accessKey + ':' + sign + ':' + data; +//} +// +//Mac.prototype.sign_request = function(path, body, content_type) { +// var u = url.parse(path); +// var path = u.path; +// var data = path + '\n'; +// +// if (body) { +// data += body; +// } +// +// return this.access + ':' + this._sign(data); +//} +// + diff --git a/qiniu/conf.js b/qiniu/conf.js new file mode 100644 index 00000000..e009a789 --- /dev/null +++ b/qiniu/conf.js @@ -0,0 +1,9 @@ + +exports.ACCESS_KEY = ''; +exports.SECRET_KEY = ''; + +exports.USER_AGENT = 'qiniu nodejs-sdk v6.0.0'; + +exports.UP_HOST = 'http://up.qbox.me'; +exports.RS_HOST = 'http://rs.qbox.me'; +exports.RSF_HOST = 'http://rsf.qbox.me'; diff --git a/qiniu/fop.js b/qiniu/fop.js new file mode 100644 index 00000000..860c7f54 --- /dev/null +++ b/qiniu/fop.js @@ -0,0 +1,51 @@ + + +exports.ImageView = ImageView; +exports.ImageInfo = ImageInfo; +exports.Exif = Exif; + +function ImageView(mode, width, height, quality, format) { + this.mode = mode || 1; + this.width = width || 0; + this.height = height || 0; + this.quality = quality || 0; + this.format = format || null; +} + +ImageView.prototype.makeRequest = function(url) { + url += '?imageView/' + this.mode; + + if (this.width > 0) { + url += '/w/' + this.width; + } + + if (this.height > 0) { + url += '/h/' + this.height; + } + + if (this.quality > 0) { + url += '/q/' + this.quality; + } + + if (this.format) { + url += '/format/' + this.format; + } + + return url; +} + +function ImageInfo() { +} + +ImageInfo.prototype.makeRequest = function(url) { + return url + '?imageInfo' +} + +function Exif() { +} + +Exif.prototype.makeRequest = function(url) { + return url + '?exif' +} + + diff --git a/qiniu/io.js b/qiniu/io.js new file mode 100644 index 00000000..ee8fbf41 --- /dev/null +++ b/qiniu/io.js @@ -0,0 +1,104 @@ + +var conf = require('./conf'); +var util = require('./util'); +var rpc = require('./rpc'); +var fs = require('fs'); +var getCrc32 = require('crc32'); +var url = require('url'); +var mime = require('mime'); +var formstream = require('formstream'); + +exports.UNDEFINED_KEY = '?' +exports.PutExtra = PutExtra; +exports.PutRet = PutRet; +exports.put = put; +exports.putWithoutKey = putWithoutKey; +exports.putFile = putFile; +exports.putFileWithoutKey = putFileWithoutKey; + +// @gist PutExtra +function PutExtra(params, mimeType, crc32, checkCrc) { + this.paras = params || {}; + this.mimeType = mimeType || null; + this.crc32 = crc32 || null; + this.checkCrc = checkCrc || 0; +} +// @endgist + +function PutRet(hash, key) { + this.hash = hash || null; + this.key = key || null; +} + +// onret: callback function instead of ret +function put(uptoken, key, body, extra, onret) { + if(!extra) { + extra = new PutExtra(); + } + if (!extra.mimeType) { + extra.mimeType = 'application/octet-stream'; + } + + if(!key) { + key = exports.UNDEFINED_KEY; + } + + var form = getMultipart(uptoken, key, body, extra); + + rpc.postMultipart(conf.UP_HOST, form, onret); +} + +function putWithoutKey(uptoken, body, extra, onret) { + put(uptoken, null, body, extra, onret); +} + +function getMultipart(uptoken, key, body, extra) { + + var form = formstream(); + + form.field('token', uptoken); + if(key != exports.UNDEFINED_KEY) { + form.field('key', key); + } + + form.buffer('file', new Buffer(body), key, extra.mimeType); + + //extra['checkcrc'] + if (extra.checkCrc == 1) { + var bodyCrc32 = getCrc32(body); + extra.crc32 = '' + parseInt(bodyCrc32, 16); + } + + if(extra.checkCrc) { + form.field('crc32', extra.crc32); + } + + for (k in extra.params) { + form.field(k, extra.params[k]); + } + + return form; +} + +function putFile(uptoken, key, loadFile, extra, onret) { + fs.readFile(loadFile, function(err, data) { + if(err) { + onret({code: -1, error: err.toString()}, {}); + return; + } + + if(!extra) { + extra = new PutExtra(); + } + + if(!extra.mimeType) { + extra.mimeType = mime.lookup(loadFile); + } + put(uptoken, key, data, extra, onret); + }); +} + +function putFileWithoutKey(uptoken, loadFile, extra, onret) { + putFile(uptoken, null, loadFile, extra, onret); +} + diff --git a/qiniu/rpc.js b/qiniu/rpc.js new file mode 100644 index 00000000..e1dd41aa --- /dev/null +++ b/qiniu/rpc.js @@ -0,0 +1,63 @@ +var url = require('url'); +var util = require('./util'); +var conf = require('./conf'); + +exports.postMultipart = postMultipart; +exports.postWithForm = postWithForm; +exports.postWithoutForm = postWithoutForm; + +function postMultipart(uri, form, onret) { + post(uri, form, form.headers(), util.getResp(onret)); +} + +function postWithForm(uri, form, token, onret) { + var headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + if (token) { + headers['Authorization'] = token; + } + post(uri, form, headers, util.getResp(onret)); +} + +function postWithoutForm(uri, token, onret) { + var headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + if (token) { + headers['Authorization'] = token; + } + post(uri, null, headers, util.getResp(onret)); +} + +function post(uri, form, headers, onresp) { + + var u = url.parse(uri); + var options = { + headers: headers, + method: 'POST', + host: u.hostname, + port: u.port, + path: u.path, + 'User-Agent': conf.USER_AGENT, + } + + var proto; + if (u.protocol == 'https') { + proto = require('https'); + } else { + proto = require('http'); + } + + var req = proto.request(options, onresp); + if(form) { + if (typeof form === 'string') { + req.end(form); + } else { + form.pipe(req); + } + } else { + req.end(); + } +} + diff --git a/qiniu/rs.js b/qiniu/rs.js new file mode 100644 index 00000000..d683f5ae --- /dev/null +++ b/qiniu/rs.js @@ -0,0 +1,219 @@ + +var url = require('url'); +var crypto = require('crypto'); +var formstream = require('formstream'); +var querystring = require('querystring'); +var rpc = require('./rpc'); +var conf = require('./conf'); +var util = require('./util'); +var Mac = require('./auth/digest').Mac; + + +exports.Client = Client; +exports.Entry = Entry; +exports.EntryPath = EntryPath; +exports.EntryPathPair = EntryPathPair; +exports.BatchItemRet = BatchItemRet; +exports.BatchStatItemRet = BatchStatItemRet; + +exports.PutPolicy = PutPolicy; +exports.GetPolicy = GetPolicy; +exports.makeBaseUrl = makeBaseUrl; + +function Client(client) { + this.client = client || null; +} + +Client.prototype.stat = function(bucket, key, onret) { + var encodedEntryUri = getEncodedEntryUri(bucket, key); + var uri = conf.RS_HOST + '/stat/' + encodedEntryUri; + var digest = util.generateAccessToken(uri, null); + + rpc.postWithoutForm(uri, digest, onret); +} + +Client.prototype.remove = function(bucket, key, onret) { + /* + * func (this Client) Delete(bucket, key string) (err error) + * */ + var encodedEntryUri = getEncodedEntryUri(bucket, key); + var uri = conf.RS_HOST + '/delete/' + encodedEntryUri; + var digest = util.generateAccessToken(uri, null); + rpc.postWithoutForm(uri, digest, onret); +} + +Client.prototype.move = function(bucketSrc, keySrc, bucketDest, keyDest, onret) { + var encodedEntryURISrc = getEncodedEntryUri(bucketSrc, keySrc); + var encodedEntryURIDest = getEncodedEntryUri(bucketDest, keyDest); + var uri = conf.RS_HOST + '/move/' + encodedEntryURISrc + '/' + encodedEntryURIDest; + var digest = util.generateAccessToken(uri, null); + rpc.postWithoutForm(uri, digest, onret); +} + +Client.prototype.copy = function(bucketSrc, keySrc, bucketDest, keyDest, onret) { + var encodedEntryURISrc = getEncodedEntryUri(bucketSrc, keySrc); + var encodedEntryURIDest = getEncodedEntryUri(bucketDest, keyDest); + var uri = conf.RS_HOST + '/copy/' + encodedEntryURISrc + '/' + encodedEntryURIDest; + var digest = util.generateAccessToken(uri, null); + rpc.postWithoutForm(uri, digest, onret); +} + + +function Entry(hash, fsize, putTime, mimeType, endUser) { + this.hash = hash || null; + this.fsize = fsize || null; + this.putTime = putTime || null; + this.mimeType = mimeType || null; + this.endUser = endUser || null; +} + +// ----- batch ------- + +function EntryPath(bucket, key) { + this.bucket = bucket || null; + this.key = key || null; +} + +EntryPath.prototype.encode = function() { + return getEncodedEntryUri(this.bucket, this.key); +} + +EntryPath.prototype.toStr = function(op) { + return 'op=/' + op + '/' + getEncodedEntryUri(this.bucket, this.key) + '&'; +} + +function EntryPathPair(src, dest) { + this.src = src || null; + this.dest = dest || null; +} + +EntryPathPair.prototype.toStr = function(op) { + return 'op=/' + op + '/' + this.src.encode() + '/' + this.dest.encode() + '&'; +} + +function BatchItemRet(error, code) { + this.error = error || null; + this.code = code || null; +} + +function BatchStatItemRet(data, error, code) { + this.data = data; + this.error = error; + this.code = code; +} + +Client.prototype.batchStat = function(entries, onret) { + fileHandle('stat', entries, onret); +} + +Client.prototype.batchDelete = function(entries, onret) { + fileHandle('delete', entries, onret); +} + +Client.prototype.batchMove = function(entries, onret) { + fileHandle('move', entries, onret); +} + +Client.prototype.batchCopy = function(entries, onret) { + fileHandle('copy', entries, onret); +} + +function fileHandle(op, entries, onret) { + var body = ''; + for (i in entries) { + body += entries[i].toStr(op); + } + + var uri = conf.RS_HOST + '/batch'; + var digest = util.generateAccessToken(uri, body); + rpc.postWithForm(uri, body, digest, onret); +} + +function getEncodedEntryUri(bucket, key) { + return util.urlsafeBase64Encode(bucket + ':' + key); +} + +// ----- token -------- +// @gist PutPolicy +function PutPolicy(scope, callbackUrl, callbackBody, returnUrl, returnBody, + asyncOps, endUser, expires) { + this.scope = scope || null; + this.callbackUrl = callbackUrl || null; + this.callbackBody = callbackBody || null; + this.returnUrl = returnUrl || null; + this.returnBody = returnBody || null; + this.asyncOps = asyncOps || null; + this.endUser = endUser || null; + this.expires = expires || 3600; +} +// @endgist + +PutPolicy.prototype.token = function(mac) { + if (mac == null) { + mac = new Mac(conf.ACCESS_KEY, conf.SECRET_KEY); + } + var flags = this.getFlags(); + var encodedFlags = util.urlsafeBase64Encode(JSON.stringify(flags)); + var encoded = util.hmacSha1(encodedFlags, mac.secretKey); + var encodedSign = util.base64ToUrlSafe(encoded); + var uploadToken = mac.accessKey + ':' + encodedSign + ':' + encodedFlags; + return uploadToken; +} + +PutPolicy.prototype.getFlags = function(putPolicy) { + var flags = {}; + if (this.scope != null) { + flags['scope'] = this.scope; + } + if (this.callbackUrl != null) { + flags['callbackUrl'] = this.scope; + } + if (this.callbackBody != null) { + flags['callbackBody'] = this.callbackBody; + } + if (this.returnUrl != null) { + flags['returnUrl'] = this.returnUrl; + } + if (this.returnBody != null) { + flags['returnBody'] = this.returnBody; + } + if (this.asyncOps != null) { + flags['asyncOps'] = this.asyncOps; + } + if (this.endUser != null) { + flags['endUser'] = this.endUser; + } + flags['deadline'] = this.expires + Math.floor(Date.now() / 1000); + return flags; +} + + +function GetPolicy(expires) { + this.expires = expires || 3600; +} + +GetPolicy.prototype.makeRequest = function(baseUrl, mac) { + if (!mac) { + mac = new Mac(conf.ACCESS_KEY, conf.SECRET_KEY); + } + + var deadline = this.expires + Math.floor(Date.now() / 1000); + + if (baseUrl.indexOf('?') >= 0) { + baseUrl += '&e='; + } else { + baseUrl += '?e='; + } + baseUrl += deadline; + + var signature = util.hmacSha1(baseUrl, mac.secretKey); + var encodedSign = util.base64ToUrlSafe(signature); + var downloadToken = mac.accessKey + ':' + encodedSign; + + return baseUrl + '&token=' + downloadToken; +} + +function makeBaseUrl(domain, key) { + key = new Buffer(key); + return 'http://' + domain + '/' + querystring.escape(key); +} diff --git a/qiniu/rsf.js b/qiniu/rsf.js new file mode 100644 index 00000000..b8b895a6 --- /dev/null +++ b/qiniu/rsf.js @@ -0,0 +1,35 @@ +var rpc = require('./rpc'); +var conf = require('./conf'); +var util = require('./util'); + +exports.listPrefix = function(bucket, prefix, marker, limit, onret) { + var uri = getPrefixUri(bucket, prefix, marker, limit); + var digest = util.generateAccessToken(uri, null); + + rpc.postWithoutForm(uri, digest, onret) +} + +function getPrefixUri(bucket, prefix, marker, limit) { + var uri = conf.RSF_HOST + '/' + 'list?' + 'bucket=' + bucket; + if (marker) { + uri += '&' + 'marker=' + marker; + } + + if (limit) { + uri += '&' + 'limit=' + limit; + } + + if (prefix) { + uri += '&' + 'prefix=' + prefix; + } + return uri; +} + +function ListItem(key, hash, fsize, putTime, mimeType, endUser) { + this.key = key || null; + this.hash = hash || null; + this.fsize = fsize || null; + this.putTime = putTime || null; + this.mimeType = mimeType || null; + this.endUser = endUser || null; +} diff --git a/qiniu/util.js b/qiniu/util.js new file mode 100644 index 00000000..b8e99569 --- /dev/null +++ b/qiniu/util.js @@ -0,0 +1,101 @@ +var fs = require('fs'); +var url = require('url'); +var path = require('path'); +var crypto = require('crypto'); +var conf = require('./conf'); + +// ------------------------------------------------------------------------------------------ +// func encode + +exports.urlsafeBase64Encode = function(jsonFlags) { + var encoded = new Buffer(jsonFlags).toString('base64'); + return exports.base64ToUrlSafe(encoded); +} + +exports.base64ToUrlSafe = function(v) { + return v.replace(/\//g, '_').replace(/\+/g, '-'); +} + +exports.hmacSha1 = function(encodedFlags, secretKey) { + /* + *return value already encoded with base64 + * */ + var hmac = crypto.createHmac('sha1', secretKey); + hmac.update(encodedFlags); + return hmac.digest('base64'); +} + +// ------------------------------------------------------------------------------------------ +// func readAll + +exports.readAll = function(strm, ondata) { + var out = []; + var total = 0; + strm.on('data', function(chunk) { + out.push(chunk); + total += chunk.length; + }); + strm.on('end', function() { + var data; + switch (out.length) { + case 0: + data = new Buffer(0); + break; + case 1: + data = out[0]; + break; + default: + data = new Buffer(total); + var pos = 0; + for (var i = 0; i < out.length; i++) { + var chunk = out[i]; + chunk.copy(data, pos); + pos += chunk.length; + } + } + ondata(data); + }); +}; + +// ------------------------------------------------------------------------------------------ +// func generateAccessToken + +exports.generateAccessToken = function(uri, body) { + var u = url.parse(uri); + var path = u.path; + var access = path + '\n'; + + if (body) { + access += body; + } + + var digest = exports.hmacSha1(access, conf.SECRET_KEY); + var safeDigest = exports.base64ToUrlSafe(digest); + return 'QBox ' + conf.ACCESS_KEY + ':' + safeDigest; +} + +// -------------------- +// getResp + +exports.getResp = function(onret) { + var onresp = function(res) { + exports.readAll(res, function(data) { + var err, ret; + + if (Math.floor(res.statusCode/100) === 2) { + if (data.length !== 0) { + try { + ret = JSON.parse(data); + } catch (e) { + err = {code: res.statusCode, err: e.toString()}; + } + } + } else { + err = {code: res.statusCode, error: data.toString()}; + } + onret(err, ret); + }) + }; + + return onresp; +} diff --git a/test-env.sh b/test-env.sh new file mode 100644 index 00000000..c089a872 --- /dev/null +++ b/test-env.sh @@ -0,0 +1,4 @@ +export QINIU_ACCESS_KEY="" +export QINIU_SECRET_KEY="" +export QINIU_TEST_BUCKET="" +export QINIU_TEST_DOMAIN="" diff --git a/test/fop.test.js b/test/fop.test.js new file mode 100644 index 00000000..65e685d6 --- /dev/null +++ b/test/fop.test.js @@ -0,0 +1,43 @@ +var fop = require('../').fop; + +describe('test start step 0', function() { + describe('fop.js', function() { + var pic = 'http://test963.qiniudn.com/logo.png'; + + describe('fop.Exif#makeRequest()', function() { + it('test makeRequest', function(done) { + var exif = new fop.Exif(); + var returl = exif.makeRequest(pic); + returl.should.equal(pic + '?exif'); + done(); + }); + }); + + describe('fop.ImageView#makeRequest()', function() { + it('test makeRequest of ImageView', function(done) { + var iv = new fop.ImageView(); + iv.height = 100; + iv.width = 40; + var returl = iv.makeRequest(pic); + returl.should.equal(pic + '?imageView/1/w/40/h/100'); + + iv.quality = 20; + iv.format = 'jpg'; + returl = iv.makeRequest(pic); + returl.should.equal(pic + '?imageView/1/w/40/h/100/q/20/format/jpg'); + done(); + }); + }); + + describe('fop.ImageInfo#makeRequest()', function() { + it('test makeRequest of ImageInfo', function(done) { + var ii = new fop.ImageInfo(); + var returl = ii.makeRequest(pic); + returl.should.equal(pic + '?imageInfo'); + done(); + }); + }); + + }); +}); + diff --git a/test/io.test.js b/test/io.test.js new file mode 100644 index 00000000..1d77bd5d --- /dev/null +++ b/test/io.test.js @@ -0,0 +1,130 @@ +var qiniu = require('../'); +var should = require('should'); +var path = require('path'); + +qiniu.conf.ACCESS_KEY = process.env.QINIU_ACCESS_KEY; +qiniu.conf.SECRET_KEY = process.env.QINIU_SECRET_KEY; + +var TEST_BUCKET = process.env.QINIU_TEST_BUCKET; +var TEST_DOMAIN = process.env.QINIU_TEST_DOMAIN; + +var imageFile = path.join(__dirname, 'logo.png'); + +before(function(done) { + if(!process.env.QINIU_ACCESS_KEY) { + console.log('should run command `source test-env.sh` first\n'); + process.exit(0); + } + done(); +}); + +describe('test start step1:', function() { + + var keys = []; + + after(function(done) { + entries = []; + for (i in keys) { + entries.push(new qiniu.rs.EntryPath(TEST_BUCKET, keys[i])); + } + + var client = new qiniu.rs.Client(); + client.batchDelete(entries, function(err, ret) { + should.not.exist(err); + done(); + }); + }); + + describe('io.js', function() { + describe('upload#', function() { + var uptoken = null; + beforeEach(function(done) { + var putPolicy = new qiniu.rs.PutPolicy(TEST_BUCKET); + uptoken = putPolicy.token(); + done(); + }); + + describe('io.put()', function() { + it('test upload from memory', function(done) { + var key = 'filename' + Math.random(1000); + qiniu.io.put(uptoken, key, 'content', null, function(err, ret) { + should.not.exist(err); + ret.should.have.keys('hash', 'key'); + ret.key.should.equal(key); + keys.push(ret.key); + done(); + }); + }); + }); + + describe('io.putWithoutKey()', function() { + it('test upload from memory without key', function(done) { + var content = 'content' + Math.random(1000); + qiniu.io.putWithoutKey(uptoken, content, null, function(err, ret) { + should.not.exist(err); + ret.should.have.keys('hash', 'key'); + ret.key.should.equal(ret.hash); + keys.push(ret.key); + done(); + }); + }); + }); + + describe('io.putFile()', function() { + it('test upload from a file', function(done) { + var key = Math.random() + 'logo.png'; + qiniu.io.putFile(uptoken, key, imageFile, null, function(err, ret) { + should.not.exist(err); + ret.should.have.keys('key', 'hash'); + ret.key.should.equal(key); + keys.push(ret.key); + done(); + }); + }); + + it('test upload from a file with checkCrc32=1', function(done) { + var extra = new qiniu.io.PutExtra(); + extra.checkCrc = 1; + var key = Math.random() + 'logo_crc32.png'; + qiniu.io.putFile(uptoken, key, imageFile, extra, function(err, ret) { + should.not.exist(err); + ret.should.have.keys('key', 'hash'); + ret.key.should.equal(key); + keys.push(ret.key); + done(); + }); + }); + }); + +// describe('io.putFileWithoutKey()', function() { +// it('test upload from a file without key', function(done) { +// qiniu.io.putFileWithoutKey(uptoken, imageFile, null, function(ret) { +// ret.code.should.equal(200); +// ret.data.should.have.keys('key', 'hash'); +// ret.data.key.should.equal(ret.data.hash); +// keys.push(ret.data.key); +// done(); +// }); +// }); +// }); + }); + }); + + describe('rsf.js', function() { + describe('file handle', function() { + describe('rsf.listPrefix()', function() { + it('list all file in test bucket', function(done) { + qiniu.rsf.listPrefix(TEST_BUCKET, null, null, null, function(err, ret) { + should.not.exist(err); +// ret.data.items.length.should.equal(keys.length); + for (i in ret.items) { + ret.items[i].should.have.keys('key', 'putTime', 'hash', 'fsize', 'mimeType'); +// keys.indexOf(ret.items[i].key).should.above(-1); + } + done(); + }); + }); + }); + }); + }); +}); diff --git a/test/rs.test.js b/test/rs.test.js index d2a793f1..75024600 100644 --- a/test/rs.test.js +++ b/test/rs.test.js @@ -1,493 +1,170 @@ -/*! - * qiniu - test/rs.test.js - * Copyright(c) 2012 fengmk2 - * MIT Licensed - */ - -"use strict"; - -/** - * Module dependencies. - */ - -var Stream = require('stream'); -var http = require('http'); -var fs = require('fs'); -var should = require('should'); var qiniu = require('../'); +var should = require('should'); var path = require('path'); -var pedding = require('pedding'); -var urlparse = require('url').parse; qiniu.conf.ACCESS_KEY = process.env.QINIU_ACCESS_KEY; qiniu.conf.SECRET_KEY = process.env.QINIU_SECRET_KEY; -var currentTime = new Date().getTime(); -var bucket = "qiniutest" + currentTime, - DEMO_DOMAIN = bucket + '.qiniudn.com', - imagefile = path.join(__dirname, 'logo.png'); - -var conn = new qiniu.digestauth.Client(), - rs = new qiniu.rs.Service(conn, bucket); - -describe('rs.test.js', function () { - var lastHash = null; - - before(function (done) { - qiniu.rs.mkbucket(conn, bucket, function(res){ - res.should.have.property('code', 200); - done(); - }); - }); +var TEST_BUCKET = process.env.QINIU_TEST_BUCKET; +var TEST_DOMAIN = process.env.QINIU_TEST_DOMAIN; +var imageFile = path.join(__dirname, 'logo.png'); - after(function (done) { - rs.drop(function (res) { - res.should.have.property('code', 200); - done(); - }); - }); +var logo = Math.random() + 'logo.png'; +var logo1 = Math.random() + 'logo1.png'; +var logo2 = Math.random() + 'logo2.png'; +var logo3 = Math.random() + 'logo3.png'; +describe('test start step2:', function() { - describe('putAuth()', function () { + describe('rs.test.js', function() { - it('should return the auth upload url with default expires time 3600 seconds', function (done) { - rs.putAuth(function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.keys(['expiresIn', 'url']); - res.data.expiresIn.should.equal(3600); - res.data.url.should.match(/^http:\/\/iovip\.qbox\.me\/upload\/[\w\-]+$/); - done(); - }); - }); + var client = new qiniu.rs.Client(); - }); + var EntryPath = qiniu.rs.EntryPath; + var EntryPathPair = qiniu.rs.EntryPathPair; - describe('putAuthEx()', function () { + describe('single file handle', function() { - it('should return the auth upload url with custom expires time 60 seconds and callback url', function (done) { - rs.putAuthEx(60, 'http://127.0.0.1/callback', function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.keys(['expiresIn', 'url']); - res.data.expiresIn.should.equal(60); - res.data.url.should.match(/^http:\/\/iovip\.qbox\.me\/upload\/[\w\-=]+$/); - done(); - }); - }); - - }); - - describe('putFile()', function () { - - it('should upload a file with key', function (done) { - rs.putFile('rs.test.js', null, __filename, function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.property('hash').with.match(/^[\w\-=]{28}$/); - done(); - }); - }); - - it('should return error when file not exists', function (done) { - rs.putFile('rs.test.js.not.exists', null, __filename + '123', function (res) { - res.should.have.keys('code', 'error', 'detail'); - res.code.should.equal(-1); - res.error.should.include('ENOENT'); - done(); - }); - }); - - it('should return error when req.abort()', function (done) { - var size = fs.statSync(__filename).size; - var stream = fs.createReadStream(__filename); - - var req = rs.put('rs.test.js.abort', null, stream, size, function (res) { - res.should.have.keys('code', 'detail', 'error'); - res.code.should.equal(-1); - res.error.should.equal('socket hang up'); - res.detail.code.should.equal('ECONNRESET'); - rs.get('rs.test.js.abort', 'test.js', function (res) { - res.should.have.keys('code', 'error'); - res.code.should.equal(612); - res.error.should.equal('no such file or directory'); - done(); + before(function(done) { + var putPolicy = new qiniu.rs.PutPolicy(TEST_BUCKET); + var uptoken = putPolicy.token(); + qiniu.io.putFile(uptoken, logo2, imageFile, null, function(err, ret) { + should.not.exist(err); }); - }); - - setTimeout(function () { - req.abort(); - }, 5); - }); - - }); - - describe('uploadFile() && upload()', function () { - - var upToken = null; - beforeEach(function (done) { - rs.putAuth(function (res) { - res.code.should.equal(200); - upToken = res.data.url; - done(); - }); - }); - - it('should upload a file with key using form-data format', function (done) { - rs.uploadFile(upToken, 'test/rs.test.js.uploadFile', null, __filename, function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.property('hash').with.match(/^[\w\-=]{28}$/); - var lastHash = res.data.hash; - rs.get('test/rs.test.js.uploadFile', 'foo.js', function (res) { - res.code.should.equal(200); - res.data.hash.should.equal(lastHash); - res.data.should.have.keys('expires', 'fsize', 'hash', 'mimeType', 'url'); - res.data.fsize.should.equal(fs.statSync(__filename).size); + qiniu.io.putFile(uptoken, logo, imageFile, null, function(err, ret) { + should.not.exist(err); done(); }); }); - }); - - it('should return error when file not exists', function (done) { - rs.uploadFile(upToken, 'rs.test.js.not.exists', null, __filename + '123', function (res) { - res.should.have.keys('code', 'error', 'detail'); - res.code.should.equal(-1); - res.error.should.include('ENOENT'); - done(); - }); - }); - it('should upload a stream with key using form-data format', function (done) { - var logoStream = fs.createReadStream(imagefile); - rs.upload(upToken, 'test/rs.test.js.upload.logo.png', null, 'logo.png', logoStream, - function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.property('hash').with.match(/^[\w\-=]{28}$/); - var lastHash = res.data.hash; - rs.get('test/rs.test.js.upload.logo.png', 'qiniu-logo.png', function (res) { - res.code.should.equal(200); - res.data.hash.should.equal(lastHash); - res.data.should.have.keys('expires', 'fsize', 'hash', 'mimeType', 'url'); - res.data.fsize.should.equal(fs.statSync(imagefile).size); - done(); + describe('rs.Client#stat()', function() { + it('get the stat of a file', function(done) { + client.stat(TEST_BUCKET, logo, function(err, ret) { + should.not.exist(err); + ret.should.have.keys('hash', 'fsize', 'putTime', 'mimeType'); + done(); + }); }); }); - }); - it('should upload any ReadStream using form-data format', function (done) { - var s = new Stream(); - var count = 0; - var size = 0; - var timer = setInterval(function () { - var text = 'I come from timer stream ' + count + '\n'; - size += text.length; - count++; - if (count >= 5) { - clearInterval(timer); - process.nextTick(function () { - s.emit('end'); + describe('rs.Client#copy()', function() { + it('copy logo.png to logo1.png', function(done) { + client.copy(TEST_BUCKET, logo, TEST_BUCKET, logo1, function(err, ret) { + should.not.exist(err); + done(); }); - } - s.emit('data', text); - }, 100); - - rs.upload(upToken, 'test/rs.test.js.upload.timer.stream', null, 'stream.txt', s, function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.property('hash').with.match(/^[\w\-=]{28}$/); - var lastHash = res.data.hash; - rs.get('test/rs.test.js.upload.timer.stream', 'stream.txt', function (res) { - res.code.should.equal(200); - res.data.hash.should.equal(lastHash); - res.data.should.have.keys('expires', 'fsize', 'hash', 'mimeType', 'url'); - res.data.fsize.should.equal(size); - done(); }); }); - }); - it('should upload ReadStream and abort() the request', function (done) { - var s = new Stream(); - var count = 0; - var size = 0; - var timer = setInterval(function () { - var text = 'I come from timer stream ' + count + '\n'; - size += text.length; - count++; - if (count >= 5) { - clearInterval(timer); - process.nextTick(function () { - s.emit('end'); + describe('rs.Client#remove()', function() { + it('remove logo.png', function(done) { + client.remove(TEST_BUCKET, logo, function(err, ret) { + should.not.exist(err); + done(); }); - } - s.emit('data', text); - }, 1000); - - var req = rs.upload(upToken, 'test/rs.test.js.upload.timer.stream.abort', null, 'stream.txt', s, function (res) { - res.should.have.keys('code', 'detail', 'error'); - res.code.should.equal(-1); - res.error.should.equal('socket hang up'); - res.detail.code.should.equal('ECONNRESET'); - rs.get('test/rs.test.js.upload.timer.stream.abort', 'stream.txt', function (res) { - res.should.have.keys('code', 'error'); - res.code.should.equal(612); - res.error.should.equal('no such file or directory'); - done(); }); }); - setTimeout(function () { - req.abort(); - }, 1500); - }); - - }); - - describe('uploadWithToken() && uploadFileWithToken()', function () { - var upToken = null; - beforeEach(function (done) { - var opts = { - scope: bucket, - expires: 3600, - callbackUrl: null, - callbackBodyType: null, - customer: null - }; - var policy = new qiniu.auth.PutPolicy(opts); - upToken = policy.token(); - done(); - }); - - it('should upload a stream with key using upToken and form-date format', function (done) { - - var s = new Stream(); - var count = 0; - var size = 0; - var timer = setInterval(function () { - var text = 'I come from timer stream ' + count + '\n'; - size += text.length; - count++; - if (count >= 5) { - clearInterval(timer); - process.nextTick(function () { - s.emit('end'); + describe('rs.Client#move()', function() { + it('move logo1.png to logo.png', function(done) { + client.move(TEST_BUCKET, logo1, TEST_BUCKET, logo, function(err, ret) { + should.not.exist(err); + done(); }); - } - s.emit('data', text); - }, 100); - - rs.uploadWithToken(upToken, s, "stream.txt", null, null, null, null, function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.property('hash').with.match(/^[\w\-=]{28}$/); - var lastHash = res.data.hash; - rs.get('stream.txt', 'stream.txt', function (res) { - res.code.should.equal(200); - res.data.hash.should.equal(lastHash); - res.data.should.have.keys('expires', 'fsize', 'hash', 'mimeType', 'url'); - res.data.fsize.should.equal(size); - done(); }); }); }); - it('should upload a file with key using upToken and form-date format', function (done) { - var fstat = fs.statSync(__filename) - , size = fstat.size; + describe('batch file handle', function() { - rs.uploadFileWithToken(upToken, __filename, "uploadfilewithtoken.txt", null, null, {}, false, function(res){ - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.property('hash').with.match(/^[\w\-=]{28}$/); - var lastHash = res.data.hash; - rs.get('uploadfilewithtoken.txt', 'uploadfilewithtoken.txt', function (res) { - res.code.should.equal(200); - res.data.hash.should.equal(lastHash); - res.data.should.have.keys('expires', 'fsize', 'hash', 'mimeType', 'url'); - res.data.fsize.should.equal(size); + after(function(done) { + var entries = [new EntryPath(TEST_BUCKET, logo), new EntryPath(TEST_BUCKET, logo2)]; + + client.batchDelete(entries, function(err, ret) { + should.not.exist(err); done(); }); }); - }); - - }); - - describe('get()', function () { - - var lastHash = null; - - beforeEach(function (done) { - rs.putFile('rs.test.js.get', null, __filename, function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.property('hash').with.match(/^[\w\-=]{28}$/); - lastHash = res.data.hash; - done(); - }); - }); - - it('should return a file download url', function (done) { - rs.get('rs.test.js.get', 'download.js', function (res) { - res.code.should.equal(200); - res.data.should.have.keys('expires', 'fsize', 'hash', 'mimeType', 'url'); - res.data.expires.should.equal(3600); - res.data.fsize.should.be.a('number').with.above(0); - res.data.hash.should.match(/^[\w\-=]{28}$/); - res.data.mimeType.should.equal('application/javascript'); - res.data.url.should.match(/^http:\/\/iovip\.qbox\.me\/file\/[\w\-=]+$/); - - var options = urlparse(res.data.url); - http.get(options, function (downloadRes) { - downloadRes.statusCode.should.equal(200); - downloadRes.should.have.header('content-disposition', 'attachment; filename="download.js"'); - downloadRes.should.have.header('content-length', res.data.fsize + ''); - var size = 0; - downloadRes.on('data', function (chunk) { - size += chunk.length; - }); - downloadRes.on('end', function () { - size.should.equal(res.data.fsize); - done(); - }); + describe('rs.Client#batchStat()', function() { + it('get the stat of logo.png and logo2.png', function(done) { + var entries = [ + new EntryPath(TEST_BUCKET, logo), + new EntryPath(TEST_BUCKET, logo2)]; + + client.batchStat(entries, function(err, ret) { + should.not.exist(err); + ret.length.should.equal(2); + for (i in ret) { + ret[i].code.should.equal(200); + ret[i].data.should.have.keys('fsize', 'hash', 'mimeType', 'putTime'); + } + done(); + }); }); - }); - }); - it('should return "file modified" when hash not match', function (done) { - rs.getIfNotModified('rs.test.js.get', 'getIfNotModified.js', 'nohash', function (res) { - res.should.have.keys('error', 'code'); - res.code.should.equal(608); - res.error.should.equal('file modified'); - done(); - }); - }); + it('should return code 298 when partial ok', function(done) { - it('should return download url when hash match', function (done) { - rs.getIfNotModified('rs.test.js.get', 'getIfNotModified.js', lastHash, function (res) { - res.should.have.keys('data', 'code'); - res.code.should.equal(200); - res.data.url.should.match(/^http:\/\/iovip\.qbox\.me\/file\/[\w\-=]+$/); - done(); - }); - }); + var entries = [ + new EntryPath(TEST_BUCKET, logo), + new EntryPath(TEST_BUCKET, 'not exist file')]; - it('should return "no such file or directory" when get the not exists key', function (done) { - done = pedding(2, done); - rs.get('not exists key', 'abc', function (res) { - res.should.eql({ error: 'no such file or directory', code: 612 }); - done(); - }); - rs.getIfNotModified('not exists key', 'abc', 'hash', function (res) { - res.should.eql({ error: 'no such file or directory', code: 612 }); - done(); - }); - }); + client.batchStat(entries, function(err, ret) { + should.not.exist(err); // 298 + ret.length.should.equal(2); - }); + for (i in ret) { + if (ret[i].code !== 200) { + ret[i].code.should.equal(612); + ret[i].data.should.have.keys('error'); + } + } - describe('stat()', function () { + done(); + }); + }); - it('should return key stat info', function (done) { - rs.stat('rs.test.js.get', function (res) { - res.should.have.keys('code', 'data'); - res.code.should.equal(200); - res.data.should.have.keys('fsize', 'hash', 'mimeType', 'putTime'); - done(); }); - }); - it('should return "no such file or directory"', function (done) { - rs.stat('not exists file', function (res) { - res.should.eql({ error: 'no such file or directory', code: 612 }); - done(); - }); - }); + describe('rs.Client#batchCopy', function() { + var entries = []; + entries.push(new EntryPathPair(new EntryPath(TEST_BUCKET, logo), new EntryPath(TEST_BUCKET, logo1))); + entries.push(new EntryPathPair(new EntryPath(TEST_BUCKET, logo2), new EntryPath(TEST_BUCKET, logo3))); - }); - - describe('remove()', function () { - before(function (done) { - rs.putFile('rs.test.js.remove', null, __filename, function (res) { - res.should.have.property('code', 200); - done(); - }); - }); - - it('should remove a file by key', function (done) { - rs.remove('rs.test.js.remove', function (res) { - res.should.eql({ code: 200 }); - // remove a gain will error - rs.remove('rs.test.js.remove', function (res) { - res.should.eql({ error: 'no such file or directory', code: 612 }); - done(); + it('copy from logo, logo2 to logo1, logo3', function(done) { + client.batchCopy(entries, function(err, ret) { + should.not.exist(err); + console.log(ret); + done(); + }); }); }); - }); - it('should return "no such file or directory" when key not exists', function (done) { - rs.remove('not exists file', function (res) { - res.should.eql({ error: 'no such file or directory', code: 612 }); - done(); - }); - }); - - }); + describe('rs.Client#batchDelete', function() { + var entries = [new EntryPath(TEST_BUCKET, logo), new EntryPath(TEST_BUCKET, logo2)]; - describe('imageMogrifyAs()', function () { - - var sourceURL = ''; - before(function (done) { - rs.putFile('logo.png', null, imagefile, function (res) { - res.code.should.equal(200); - rs.get('logo.png', 'abc', function (res) { - res.should.have.property('code', 200); - sourceURL = res.data.url; - done(); + it('delete logo.png, logo2.png', function(done) { + client.batchDelete(entries, function(err, ret) { + should.not.exist(err); + done(); + }); }); }); - }); - - it('should modified a image', function (done) { - rs.imageMogrifyAs('logo.png', sourceURL, { - thumbnail: '50x50^', - auto_orient: true, - format: 'jpg', - }, function (res) { - res.should.have.property('code', 200); - res.should.have.property('data'); - res.data.should.have.property('hash').with.match(/^[\w\-=]+$/); - done(); - }); - }); - - }); - describe('publish() && unpublish()', function () { + describe('rs.Client#batchMove', function() { + var entries = []; + entries.push(new EntryPathPair(new EntryPath(TEST_BUCKET, logo1), new EntryPath(TEST_BUCKET, logo))); + entries.push(new EntryPathPair(new EntryPath(TEST_BUCKET, logo3), new EntryPath(TEST_BUCKET, logo2))); - it('should publish a domain', function (done) { - rs.publish(DEMO_DOMAIN, function (res) { - res.should.have.property('code', 200); - // again will no problem - rs.publish(DEMO_DOMAIN, function (res) { - res.should.have.property('code', 200); - done(); + it('move from logo1.png, logo3.png to logo.png, logo2.png', function(done) { + client.batchMove(entries, function(err, ret) { + should.not.exist(err); + done(); + }); }); }); }); - it('should unpublish a domain', function (done) { - rs.unpublish(DEMO_DOMAIN, function (res) { - res.should.have.property('code', 200); - // again will error - rs.unpublish(DEMO_DOMAIN, function (res) { - res.should.eql({ error: 'Document not found', code: 599 }); - done(); - }); - }); - }); + // rs.GetPolicy#makeRequest() }); - });