mirror of
https://github.com/eiblog/eiblog.git
synced 2026-03-01 00:34:58 +08:00
add vendor
This commit is contained in:
Generated
+24
@@ -0,0 +1,24 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
language: go
|
||||
before_script:
|
||||
- export QINIU_KODO_TEST=1
|
||||
- export QINIU_ACCESS_KEY="QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm"
|
||||
- export QINIU_SECRET_KEY="Bxckh6FA-Fbs9Yt3i3cbKVK22UPBmAOHJcL95pGz"
|
||||
- export QINIU_TEST_BUCKET="gosdk"
|
||||
- export QINIU_TEST_DOMAIN="gosdk.qiniudn.com"
|
||||
install:
|
||||
- export QINIU_SRC=$HOME/gopath/src
|
||||
- mkdir -p $QINIU_SRC/qiniupkg.com
|
||||
- mv $QINIU_SRC/github.com/qiniu/api.v7 $QINIU_SRC/qiniupkg.com/api.v7
|
||||
- git clone --depth=50 git://github.com/qiniu/x.git $QINIU_SRC/qiniupkg.com/x
|
||||
- export TRAVIS_BUILD_DIR=$QINIU_SRC/qiniupkg.com/api.v7
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
- go get golang.org/x/net/context
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#Changelog
|
||||
|
||||
## 7.1.0 (2016-6-22)
|
||||
|
||||
### 增加
|
||||
* 增加多机房相关功能
|
||||
|
||||
## 7.0.5 (2015-11-20)
|
||||
|
||||
### 增加
|
||||
* add delimiter support to Bucket.List
|
||||
* 增加回调校验
|
||||
|
||||
## 7.0.4 (2015-09-03)
|
||||
|
||||
### 增加
|
||||
* 上传返回参数PutRet增加PersistentId,用于获取上传对应的fop操作的id
|
||||
|
||||
### 修复
|
||||
* token 覆盖问题
|
||||
|
||||
## 7.0.3 (2015-07-11)
|
||||
|
||||
### 增加
|
||||
* support NestedObject
|
||||
|
||||
## 7.0.2 (2015-07-7-10)
|
||||
|
||||
### 增加
|
||||
* 增加跨空间移动文件(Bucket.MoveEx)
|
||||
|
||||
## 7.0.1 (2015-07-7-10)
|
||||
|
||||
### 增加
|
||||
* 完善 PutPolicy:支持 MimeLimit、CallbackHost、CallbackFetchKey、 CallbackBodyType、 Checksum
|
||||
|
||||
## 7.0.0 (2016-06-29)
|
||||
|
||||
* 重构,初始版本
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
qiniupkg.com/api.v7 (Qiniu Go SDK v7.x)
|
||||
===============
|
||||
|
||||
[](https://travis-ci.org/qiniu/api.v7) [](https://godoc.org/qiniupkg.com/api.v7)
|
||||
|
||||
[](http://qiniu.com/)
|
||||
|
||||
# 下载
|
||||
|
||||
```
|
||||
go get -u qiniupkg.com/api.v7
|
||||
```
|
||||
如果碰到golang.org/x/net/context 不能下载,请把 http://devtools.qiniu.com/golang.org.x.net.context.tgz 下载到代码目录下并解压到src目录,或者直接下载全部 http://devtools.qiniu.com/qiniu_api_v7.tgz。
|
||||
|
||||
# 使用文档
|
||||
|
||||
## KODO Blob Storage (七牛对象存储)
|
||||
|
||||
* [qiniupkg.com/api.v7/kodo](http://godoc.org/qiniupkg.com/api.v7/kodo)
|
||||
* [qiniupkg.com/api.v7/kodocli](http://godoc.org/qiniupkg.com/api.v7/kodocli)
|
||||
|
||||
如果您是在业务服务器(服务器端)调用七牛云存储的服务,请使用 [qiniupkg.com/api.v7/kodo](http://godoc.org/qiniupkg.com/api.v7/kodo)。
|
||||
|
||||
如果您是在客户端(比如:Android/iOS 设备、Windows/Mac/Linux 桌面环境)调用七牛云存储的服务,请使用 [qiniupkg.com/api.v7/kodocli](http://godoc.org/qiniupkg.com/api.v7/kodocli)。注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,会影响您的数据安全。
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"qiniupkg.com/x/rpc.v7"
|
||||
|
||||
"sync"
|
||||
|
||||
. "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const DefaultApiHost string = "http://uc.qbox.me"
|
||||
|
||||
var (
|
||||
bucketMu sync.RWMutex
|
||||
bucketCache = make(map[string]BucketInfo)
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
*rpc.Client
|
||||
host string
|
||||
scheme string
|
||||
}
|
||||
|
||||
func NewClient(host string, scheme string) *Client {
|
||||
if host == "" {
|
||||
host = DefaultApiHost
|
||||
}
|
||||
client := rpc.DefaultClient
|
||||
return &Client{&client, host, scheme}
|
||||
}
|
||||
|
||||
type BucketInfo struct {
|
||||
UpHosts []string `json:"up"`
|
||||
IoHost string `json:"io"`
|
||||
Expire int64 `json:"expire"` // expire == 0 means no expire
|
||||
}
|
||||
|
||||
func (p *Client) GetBucketInfo(ak, bucketName string) (ret BucketInfo, err error) {
|
||||
key := ak + ":" + bucketName + ":" + p.scheme
|
||||
bucketMu.RLock()
|
||||
bucketInfo, ok := bucketCache[key]
|
||||
bucketMu.RUnlock()
|
||||
if ok && (bucketInfo.Expire == 0 || bucketInfo.Expire > time.Now().Unix()) {
|
||||
ret = bucketInfo
|
||||
return
|
||||
}
|
||||
hostInfo, err := p.bucketHosts(ak, bucketName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ret.Expire = time.Now().Unix() + hostInfo.Ttl
|
||||
if p.scheme == "https" {
|
||||
ret.UpHosts = hostInfo.Https["up"]
|
||||
if iohosts, ok := hostInfo.Https["io"]; ok && len(iohosts) != 0 {
|
||||
ret.IoHost = iohosts[0]
|
||||
}
|
||||
} else {
|
||||
ret.UpHosts = hostInfo.Http["up"]
|
||||
if iohosts, ok := hostInfo.Http["io"]; ok && len(iohosts) != 0 {
|
||||
ret.IoHost = iohosts[0]
|
||||
}
|
||||
}
|
||||
bucketMu.Lock()
|
||||
bucketCache[key] = ret
|
||||
bucketMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
type HostsInfo struct {
|
||||
Ttl int64 `json:"ttl"`
|
||||
Http map[string][]string `json:"http"`
|
||||
Https map[string][]string `json:"https"`
|
||||
}
|
||||
|
||||
/*
|
||||
请求包:
|
||||
GET /v1/query?ak=<ak>&&bucket=<bucket>
|
||||
返回包:
|
||||
200 OK {
|
||||
"ttl": <ttl>, // 有效时间
|
||||
"http": {
|
||||
"up": [],
|
||||
"io": [], // 当bucket为global时,我们不需要iohost, io缺省
|
||||
},
|
||||
"https": {
|
||||
"up": [],
|
||||
"io": [], // 当bucket为global时,我们不需要iohost, io缺省
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (p *Client) bucketHosts(ak, bucket string) (info HostsInfo, err error) {
|
||||
ctx := Background()
|
||||
err = p.CallWithForm(ctx, &info, "GET", p.host+"/v1/query", map[string][]string{
|
||||
"ak": []string{ak},
|
||||
"bucket": []string{bucket},
|
||||
})
|
||||
return
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package qbox
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
. "qiniupkg.com/api.v7/conf"
|
||||
"qiniupkg.com/x/bytes.v7/seekable"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type Mac struct {
|
||||
AccessKey string
|
||||
SecretKey []byte
|
||||
}
|
||||
|
||||
func NewMac(accessKey, secretKey string) (mac *Mac) {
|
||||
|
||||
if accessKey == "" {
|
||||
accessKey, secretKey = ACCESS_KEY, SECRET_KEY
|
||||
}
|
||||
return &Mac{accessKey, []byte(secretKey)}
|
||||
}
|
||||
|
||||
func (mac *Mac) Sign(data []byte) (token string) {
|
||||
|
||||
h := hmac.New(sha1.New, mac.SecretKey)
|
||||
h.Write(data)
|
||||
|
||||
sign := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
return mac.AccessKey + ":" + sign[:27]
|
||||
}
|
||||
|
||||
func (mac *Mac) SignWithData(b []byte) (token string) {
|
||||
|
||||
blen := base64.URLEncoding.EncodedLen(len(b))
|
||||
|
||||
key := mac.AccessKey
|
||||
nkey := len(key)
|
||||
ret := make([]byte, nkey+30+blen)
|
||||
|
||||
base64.URLEncoding.Encode(ret[nkey+30:], b)
|
||||
|
||||
h := hmac.New(sha1.New, mac.SecretKey)
|
||||
h.Write(ret[nkey+30:])
|
||||
digest := h.Sum(nil)
|
||||
|
||||
copy(ret, key)
|
||||
ret[nkey] = ':'
|
||||
base64.URLEncoding.Encode(ret[nkey+1:], digest)
|
||||
ret[nkey+29] = ':'
|
||||
|
||||
return string(ret)
|
||||
}
|
||||
|
||||
func (mac *Mac) SignRequest(req *http.Request, incbody bool) (token string, err error) {
|
||||
|
||||
h := hmac.New(sha1.New, mac.SecretKey)
|
||||
|
||||
u := req.URL
|
||||
data := u.Path
|
||||
if u.RawQuery != "" {
|
||||
data += "?" + u.RawQuery
|
||||
}
|
||||
io.WriteString(h, data+"\n")
|
||||
|
||||
if incbody {
|
||||
s2, err2 := seekable.New(req)
|
||||
if err2 != nil {
|
||||
return "", err2
|
||||
}
|
||||
h.Write(s2.Bytes())
|
||||
}
|
||||
|
||||
sign := base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
token = mac.AccessKey + ":" + sign
|
||||
return
|
||||
}
|
||||
|
||||
func (mac *Mac) VerifyCallback(req *http.Request) (bool, error) {
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
token, err := mac.SignRequest(req, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return auth == "QBox "+token, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
func Sign(mac *Mac, data []byte) string {
|
||||
|
||||
if mac == nil {
|
||||
mac = NewMac(ACCESS_KEY, SECRET_KEY)
|
||||
}
|
||||
return mac.Sign(data)
|
||||
}
|
||||
|
||||
func SignWithData(mac *Mac, data []byte) string {
|
||||
|
||||
if mac == nil {
|
||||
mac = NewMac(ACCESS_KEY, SECRET_KEY)
|
||||
}
|
||||
return mac.SignWithData(data)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
type Transport struct {
|
||||
mac Mac
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func incBody(req *http.Request) bool {
|
||||
|
||||
if req.Body == nil {
|
||||
return false
|
||||
}
|
||||
if ct, ok := req.Header["Content-Type"]; ok {
|
||||
switch ct[0] {
|
||||
case "application/x-www-form-urlencoded":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Transport) NestedObject() interface{} {
|
||||
|
||||
return t.Transport
|
||||
}
|
||||
|
||||
func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
|
||||
token, err := t.mac.SignRequest(req, incBody(req))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("Authorization", "QBox "+token)
|
||||
return t.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func NewTransport(mac *Mac, transport http.RoundTripper) *Transport {
|
||||
|
||||
if mac == nil {
|
||||
mac = NewMac(ACCESS_KEY, SECRET_KEY)
|
||||
}
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
t := &Transport{mac: *mac, Transport: transport}
|
||||
return t
|
||||
}
|
||||
|
||||
func NewClient(mac *Mac, transport http.RoundTripper) *http.Client {
|
||||
|
||||
t := NewTransport(mac, transport)
|
||||
return &http.Client{Transport: t}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"qiniupkg.com/x/ctype.v7"
|
||||
"qiniupkg.com/x/rpc.v7"
|
||||
)
|
||||
|
||||
var version = "7.1.0"
|
||||
|
||||
var ACCESS_KEY string
|
||||
var SECRET_KEY string
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
const (
|
||||
ctypeAppName = ctype.ALPHA | ctype.DIGIT | ctype.UNDERLINE | ctype.SPACE_BAR | ctype.SUB | ctype.DOT
|
||||
)
|
||||
|
||||
// userApp should be [A-Za-z0-9_\ \-\.]*
|
||||
//
|
||||
func SetAppName(userApp string) error {
|
||||
if userApp != "" && !ctype.IsType(ctypeAppName, userApp) {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
rpc.UserAgent = fmt.Sprintf(
|
||||
"QiniuGo/%s (%s; %s; %s) %s", version, runtime.GOOS, runtime.GOARCH, userApp, runtime.Version())
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
SetAppName("")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"qiniupkg.com/x/rpc.v7"
|
||||
)
|
||||
|
||||
func TestUA(t *testing.T) {
|
||||
err := SetAppName("")
|
||||
if err != nil {
|
||||
t.Fatal("expect no error")
|
||||
}
|
||||
err = SetAppName("错误的UA")
|
||||
if err == nil {
|
||||
t.Fatal("expect an invalid ua format")
|
||||
}
|
||||
err = SetAppName("Test0-_.")
|
||||
if err != nil {
|
||||
t.Fatal("expect no error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
str := "tesT0.-_"
|
||||
SetAppName(str)
|
||||
v := rpc.UserAgent
|
||||
if !strings.Contains(v, str) {
|
||||
t.Fatal("should include user")
|
||||
}
|
||||
if !strings.HasPrefix(v, "QiniuGo/"+version) {
|
||||
t.Fatal("invalid format")
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
包 qiniupkg.com/api.v7 是七牛 Go 语言 SDK v7.x 版本
|
||||
|
||||
七牛对象存储,我们取了一个好听的名字,叫 KODO Blob Storage。要使用它,你主要和以下两个包打交道:
|
||||
|
||||
import "qiniupkg.com/api.v7/kodo"
|
||||
import "qiniupkg.com/api.v7/kodocli"
|
||||
|
||||
如果您是在业务服务器(服务器端)调用七牛云存储的服务,请使用 qiniupkg.com/api.v7/kodo。
|
||||
|
||||
如果您是在客户端(比如:Android/iOS 设备、Windows/Mac/Linux 桌面环境)调用七牛云存储的服务,请使用 qiniupkg.com/api.v7/kodocli。
|
||||
注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,
|
||||
会影响您的数据安全。
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
_ "qiniupkg.com/api.v7/auth/qbox"
|
||||
_ "qiniupkg.com/api.v7/conf"
|
||||
_ "qiniupkg.com/api.v7/kodo"
|
||||
_ "qiniupkg.com/api.v7/kodocli"
|
||||
)
|
||||
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
package kodo
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
. "golang.org/x/net/context"
|
||||
"qiniupkg.com/api.v7/api"
|
||||
"qiniupkg.com/x/log.v7"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 批量操作。
|
||||
//
|
||||
func (p *Client) Batch(ctx Context, ret interface{}, op []string) (err error) {
|
||||
|
||||
return p.CallWithForm(ctx, ret, "POST", p.RSHost+"/batch", map[string][]string{"op": op})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type Bucket struct {
|
||||
api.BucketInfo
|
||||
Conn *Client
|
||||
Name string
|
||||
}
|
||||
|
||||
// 取七牛空间(bucket)的对象实例。
|
||||
//
|
||||
// name 是创建该七牛空间(bucket)时采用的名称。
|
||||
//
|
||||
func (p *Client) Bucket(name string) Bucket {
|
||||
b, err := p.BucketWithSafe(name)
|
||||
if err != nil {
|
||||
log.Errorf("Bucket(%s) failed: %+v", name, err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (p *Client) BucketWithSafe(name string) (Bucket, error) {
|
||||
var info api.BucketInfo
|
||||
if len(p.UpHosts) == 0 {
|
||||
var err error
|
||||
info, err = p.apiCli.GetBucketInfo(p.mac.AccessKey, name)
|
||||
if err != nil {
|
||||
return Bucket{}, err
|
||||
}
|
||||
} else {
|
||||
info.IoHost = p.IoHost
|
||||
info.UpHosts = p.UpHosts
|
||||
}
|
||||
return Bucket{info, p, name}, nil
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Hash string `json:"hash"`
|
||||
Fsize int64 `json:"fsize"`
|
||||
PutTime int64 `json:"putTime"`
|
||||
MimeType string `json:"mimeType"`
|
||||
EndUser string `json:"endUser"`
|
||||
}
|
||||
|
||||
// 取文件属性。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// key 是要访问的文件的访问路径。
|
||||
//
|
||||
func (p Bucket) Stat(ctx Context, key string) (entry Entry, err error) {
|
||||
err = p.Conn.Call(ctx, &entry, "POST", p.Conn.RSHost+URIStat(p.Name, key))
|
||||
return
|
||||
}
|
||||
|
||||
// 删除一个文件。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// key 是要删除的文件的访问路径。
|
||||
//
|
||||
func (p Bucket) Delete(ctx Context, key string) (err error) {
|
||||
return p.Conn.Call(ctx, nil, "POST", p.Conn.RSHost+URIDelete(p.Name, key))
|
||||
}
|
||||
|
||||
// 移动一个文件。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// keySrc 是要移动的文件的旧路径。
|
||||
// keyDest 是要移动的文件的新路径。
|
||||
//
|
||||
func (p Bucket) Move(ctx Context, keySrc, keyDest string) (err error) {
|
||||
return p.Conn.Call(ctx, nil, "POST", p.Conn.RSHost+URIMove(p.Name, keySrc, p.Name, keyDest))
|
||||
}
|
||||
|
||||
// 跨空间(bucket)移动一个文件。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// keySrc 是要移动的文件的旧路径。
|
||||
// bucketDest 是文件的目标空间。
|
||||
// keyDest 是要移动的文件的新路径。
|
||||
//
|
||||
func (p Bucket) MoveEx(ctx Context, keySrc, bucketDest, keyDest string) (err error) {
|
||||
return p.Conn.Call(ctx, nil, "POST", p.Conn.RSHost+URIMove(p.Name, keySrc, bucketDest, keyDest))
|
||||
}
|
||||
|
||||
// 复制一个文件。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// keySrc 是要复制的文件的源路径。
|
||||
// keyDest 是要复制的文件的目标路径。
|
||||
//
|
||||
func (p Bucket) Copy(ctx Context, keySrc, keyDest string) (err error) {
|
||||
return p.Conn.Call(ctx, nil, "POST", p.Conn.RSHost+URICopy(p.Name, keySrc, p.Name, keyDest))
|
||||
}
|
||||
|
||||
// 修改文件的MIME类型。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// key 是要修改的文件的访问路径。
|
||||
// mime 是要设置的新MIME类型。
|
||||
//
|
||||
func (p Bucket) ChangeMime(ctx Context, key, mime string) (err error) {
|
||||
return p.Conn.Call(ctx, nil, "POST", p.Conn.RSHost+URIChangeMime(p.Name, key, mime))
|
||||
}
|
||||
|
||||
// 从网上抓取一个资源并存储到七牛空间(bucket)中。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// key 是要存储的文件的访问路径。如果文件已经存在则覆盖。
|
||||
// url 是要抓取的资源的URL。
|
||||
//
|
||||
func (p Bucket) Fetch(ctx Context, key string, url string) (err error) {
|
||||
return p.Conn.Call(ctx, nil, "POST", p.IoHost+uriFetch(p.Name, key, url))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type ListItem struct {
|
||||
Key string `json:"key"`
|
||||
Hash string `json:"hash"`
|
||||
Fsize int64 `json:"fsize"`
|
||||
PutTime int64 `json:"putTime"`
|
||||
MimeType string `json:"mimeType"`
|
||||
EndUser string `json:"endUser"`
|
||||
}
|
||||
|
||||
// 首次请求,请将 marker 设置为 ""。
|
||||
// 无论 err 值如何,均应该先看 entries 是否有内容。
|
||||
// 如果后续没有更多数据,err 返回 EOF,markerOut 返回 ""(但不通过该特征来判断是否结束)。
|
||||
//
|
||||
func (p Bucket) List(
|
||||
ctx Context, prefix, delimiter, marker string, limit int) (entries []ListItem, commonPrefixes []string, markerOut string, err error) {
|
||||
|
||||
listUrl := p.makeListURL(prefix, delimiter, marker, limit)
|
||||
|
||||
var listRet struct {
|
||||
Marker string `json:"marker"`
|
||||
Items []ListItem `json:"items"`
|
||||
Prefixes []string `json:"commonPrefixes"`
|
||||
}
|
||||
err = p.Conn.Call(ctx, &listRet, "POST", listUrl)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if listRet.Marker == "" {
|
||||
return listRet.Items, listRet.Prefixes, "", io.EOF
|
||||
}
|
||||
return listRet.Items, listRet.Prefixes, listRet.Marker, nil
|
||||
}
|
||||
|
||||
func (p Bucket) makeListURL(prefix, delimiter, marker string, limit int) string {
|
||||
|
||||
query := make(url.Values)
|
||||
query.Add("bucket", p.Name)
|
||||
if prefix != "" {
|
||||
query.Add("prefix", prefix)
|
||||
}
|
||||
if delimiter != "" {
|
||||
query.Add("delimiter", delimiter)
|
||||
}
|
||||
if marker != "" {
|
||||
query.Add("marker", marker)
|
||||
}
|
||||
if limit > 0 {
|
||||
query.Add("limit", strconv.FormatInt(int64(limit), 10))
|
||||
}
|
||||
return p.Conn.RSFHost + "/list?" + query.Encode()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type BatchStatItemRet struct {
|
||||
Data Entry `json:"data"`
|
||||
Error string `json:"error"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
func (p Bucket) BatchStat(ctx Context, keys ...string) (ret []BatchStatItemRet, err error) {
|
||||
|
||||
b := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
b[i] = URIStat(p.Name, key)
|
||||
}
|
||||
err = p.Conn.Batch(ctx, &ret, b)
|
||||
return
|
||||
}
|
||||
|
||||
type BatchItemRet struct {
|
||||
Error string `json:"error"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
func (p Bucket) BatchDelete(ctx Context, keys ...string) (ret []BatchItemRet, err error) {
|
||||
|
||||
b := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
b[i] = URIDelete(p.Name, key)
|
||||
}
|
||||
err = p.Conn.Batch(ctx, &ret, b)
|
||||
return
|
||||
}
|
||||
|
||||
type KeyPair struct {
|
||||
Src string
|
||||
Dest string
|
||||
}
|
||||
|
||||
func (p Bucket) BatchMove(ctx Context, entries ...KeyPair) (ret []BatchItemRet, err error) {
|
||||
|
||||
b := make([]string, len(entries))
|
||||
for i, e := range entries {
|
||||
b[i] = URIMove(p.Name, e.Src, p.Name, e.Dest)
|
||||
}
|
||||
err = p.Conn.Batch(ctx, &ret, b)
|
||||
return
|
||||
}
|
||||
|
||||
func (p Bucket) BatchCopy(ctx Context, entries ...KeyPair) (ret []BatchItemRet, err error) {
|
||||
|
||||
b := make([]string, len(entries))
|
||||
for i, e := range entries {
|
||||
b[i] = URICopy(p.Name, e.Src, p.Name, e.Dest)
|
||||
}
|
||||
err = p.Conn.Batch(ctx, &ret, b)
|
||||
return
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func encodeURI(uri string) string {
|
||||
return base64.URLEncoding.EncodeToString([]byte(uri))
|
||||
}
|
||||
|
||||
func uriFetch(bucket, key, url string) string {
|
||||
return "/fetch/" + encodeURI(url) + "/to/" + encodeURI(bucket+":"+key)
|
||||
}
|
||||
|
||||
func URIDelete(bucket, key string) string {
|
||||
return "/delete/" + encodeURI(bucket+":"+key)
|
||||
}
|
||||
|
||||
func URIStat(bucket, key string) string {
|
||||
return "/stat/" + encodeURI(bucket+":"+key)
|
||||
}
|
||||
|
||||
func URICopy(bucketSrc, keySrc, bucketDest, keyDest string) string {
|
||||
return "/copy/" + encodeURI(bucketSrc+":"+keySrc) + "/" + encodeURI(bucketDest+":"+keyDest)
|
||||
}
|
||||
|
||||
func URIMove(bucketSrc, keySrc, bucketDest, keyDest string) string {
|
||||
return "/move/" + encodeURI(bucketSrc+":"+keySrc) + "/" + encodeURI(bucketDest+":"+keyDest)
|
||||
}
|
||||
|
||||
func URIChangeMime(bucket, key, mime string) string {
|
||||
return "/chgm/" + encodeURI(bucket+":"+key) + "/mime/" + encodeURI(mime)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package kodo
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
bkey = "abatch"
|
||||
bnewkey1 = "abatch/newkey1"
|
||||
bnewkey2 = "abatch/newkey2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
if skipTest() {
|
||||
return
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
bkey += strconv.Itoa(rand.Int())
|
||||
bnewkey1 += strconv.Itoa(rand.Int())
|
||||
bnewkey2 += strconv.Itoa(rand.Int())
|
||||
// 删除 可能存在的 key
|
||||
bucket.BatchDelete(nil, bkey, bnewkey1, bnewkey2)
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
|
||||
if skipTest() {
|
||||
return
|
||||
}
|
||||
|
||||
//上传一个文件用用于测试
|
||||
err := upFile("bucket_test.go", bkey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer bucket.Delete(nil, bkey)
|
||||
|
||||
testBatchStat(t)
|
||||
testBatchCopy(t)
|
||||
testBatchMove(t)
|
||||
testBatchDelete(t)
|
||||
testBatch(t)
|
||||
testClient_MakeUptokenBucket(t)
|
||||
}
|
||||
|
||||
func testBatchStat(t *testing.T) {
|
||||
|
||||
rets, err := bucket.BatchStat(nil, bkey, bkey, bkey)
|
||||
if err != nil {
|
||||
t.Fatal("bucket.BatchStat failed:", err)
|
||||
}
|
||||
|
||||
if len(rets) != 3 {
|
||||
t.Fatal("BatchStat failed: len(rets) = ", 3)
|
||||
}
|
||||
|
||||
stat, err := bucket.Stat(nil, bkey)
|
||||
if err != nil {
|
||||
t.Fatal("bucket.Stat failed:", err)
|
||||
}
|
||||
|
||||
if rets[0].Data != stat || rets[1].Data != stat || rets[2].Data != stat {
|
||||
t.Fatal("BatchStat failed : returns err")
|
||||
}
|
||||
}
|
||||
|
||||
func testBatchMove(t *testing.T) {
|
||||
|
||||
stat0, err := bucket.Stat(nil, bkey)
|
||||
if err != nil {
|
||||
t.Fatal("BathMove get stat failed:", err)
|
||||
}
|
||||
|
||||
_, err = bucket.BatchMove(nil, KeyPair{bkey, bnewkey1}, KeyPair{bnewkey1, bnewkey2})
|
||||
if err != nil {
|
||||
t.Fatal("bucket.BatchMove failed:", err)
|
||||
}
|
||||
defer bucket.Move(nil, bnewkey2, bkey)
|
||||
|
||||
stat1, err := bucket.Stat(nil, bnewkey2)
|
||||
if err != nil {
|
||||
t.Fatal("BathMove get stat failed:", err)
|
||||
}
|
||||
|
||||
if stat0.Hash != stat1.Hash {
|
||||
t.Fatal("BatchMove failed : Move err", stat0, stat1)
|
||||
}
|
||||
}
|
||||
|
||||
func testBatchCopy(t *testing.T) {
|
||||
|
||||
_, err := bucket.BatchCopy(nil, KeyPair{bkey, bnewkey1}, KeyPair{bkey, bnewkey2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer bucket.Delete(nil, bnewkey1)
|
||||
defer bucket.Delete(nil, bnewkey2)
|
||||
|
||||
stat0, _ := bucket.Stat(nil, bkey)
|
||||
stat1, _ := bucket.Stat(nil, bnewkey1)
|
||||
stat2, _ := bucket.Stat(nil, bnewkey2)
|
||||
if stat0.Hash != stat1.Hash || stat0.Hash != stat2.Hash {
|
||||
t.Fatal("BatchCopy failed : Copy err")
|
||||
}
|
||||
}
|
||||
|
||||
func testBatchDelete(t *testing.T) {
|
||||
|
||||
bucket.Copy(nil, bkey, bnewkey1)
|
||||
bucket.Copy(nil, bkey, bnewkey2)
|
||||
|
||||
_, err := bucket.BatchDelete(nil, bnewkey1, bnewkey2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err1 := bucket.Stat(nil, bnewkey1)
|
||||
_, err2 := bucket.Stat(nil, bnewkey2)
|
||||
|
||||
//这里 err1 != nil,否则文件没被成功删除
|
||||
if err1 == nil || err2 == nil {
|
||||
t.Fatal("BatchDelete failed : File do not delete")
|
||||
}
|
||||
}
|
||||
|
||||
func testBatch(t *testing.T) {
|
||||
|
||||
ops := []string{
|
||||
URICopy(bucketName, bkey, bucketName, bnewkey1),
|
||||
URIDelete(bucketName, bkey),
|
||||
URIMove(bucketName, bnewkey1, bucketName, bkey),
|
||||
}
|
||||
|
||||
var rets []BatchItemRet
|
||||
err := client.Batch(nil, &rets, ops)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rets) != 3 {
|
||||
t.Fatal("len(rets) != 3")
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
包 qiniupkg.com/api.v7/kodo 提供了在您的业务服务器(服务端)调用七牛云存储服务的能力
|
||||
|
||||
首先,我们要配置下 AccessKey/SecretKey,这可以在七牛 Portal 中查到:
|
||||
|
||||
kodo.SetMac("your-access-key", "your-secret-key")
|
||||
|
||||
然后我们创建一个 Client 对象:
|
||||
|
||||
zone := 0 // 您空间(Bucket)所在的区域
|
||||
c := kodo.New(zone, nil) // 用默认配置创建 Client
|
||||
|
||||
有了 Client,你就可以操作您的空间(Bucket)了,比如我们要上传一个文件:
|
||||
|
||||
import "golang.org/x/net/context"
|
||||
|
||||
bucket := c.Bucket("your-bucket-name")
|
||||
ctx := context.Background()
|
||||
...
|
||||
localFile := "/your/local/image/file.jpg"
|
||||
err := bucket.PutFile(ctx, nil, "foo/bar.jpg", localFile, nil)
|
||||
if err != nil {
|
||||
... // 上传文件失败处理
|
||||
return
|
||||
}
|
||||
// 上传文件成功
|
||||
// 这时登录七牛Portal,在 your-bucket-name 空间就可以看到一个 foo/bar.jpg 的文件了
|
||||
|
||||
当然,除了上传文件,各种空间(Bucket)相关的操作都可以有,最常见自然是增删改查了:
|
||||
|
||||
entry, err := bucket.Stat(ctx, "foo/bar.jpg") // 看看空间中是否存在某个文件,其属性是什么
|
||||
bucket.Delete(ctx, "foo/bar.jpg") // 删除空间中的某个文件
|
||||
bucket.ChangeMime(ctx, "foo/bar.jpg", "image/jpeg") // 修改某个文件的 MIME 属性
|
||||
bucket.Move(ctx, "foo/bar.jpg", "new-name.jpg") // 移动文件
|
||||
bucket.Copy(ctx, "foo/bar.jpg", "new-copy-file.jpg") // 复制文件
|
||||
|
||||
等等... 请问怎么下载文件?如果是公开文件,我们只需要:
|
||||
|
||||
import "net/http"
|
||||
|
||||
domain := "domain-of-your-bucket.com" // 您的空间绑定的域名,这个可以在七牛的Portal中查到
|
||||
baseUrl := kodo.MakeBaseUrl(domain, "foo/bar.jpg") // 得到下载 url
|
||||
resp, err := http.Get(baseUrl)
|
||||
...
|
||||
|
||||
但是对于私有空间,事情要复杂一些,访问上面的 baseUrl 会被拒绝。我们需要多做一步:
|
||||
|
||||
privateUrl := c.MakePrivateUrl(baseUrl, nil) // 用默认的下载策略去生成私有下载的 url
|
||||
resp, err := http.Get(privateUrl)
|
||||
...
|
||||
*/
|
||||
package kodo
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package kodo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"qiniupkg.com/api.v7/api"
|
||||
"qiniupkg.com/api.v7/auth/qbox"
|
||||
"qiniupkg.com/api.v7/conf"
|
||||
"qiniupkg.com/x/rpc.v7"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type zoneConfig struct {
|
||||
IoHost string
|
||||
UpHosts []string
|
||||
}
|
||||
|
||||
var zones = []zoneConfig{
|
||||
// z0 华东机房:
|
||||
{
|
||||
IoHost: "http://iovip.qbox.me",
|
||||
UpHosts: []string{
|
||||
"http://up.qiniu.com",
|
||||
"http://upload.qiniu.com",
|
||||
"-H up.qiniu.com http://183.136.139.16",
|
||||
},
|
||||
},
|
||||
// z1 华北机房:
|
||||
{
|
||||
IoHost: "http://iovip-z1.qbox.me",
|
||||
UpHosts: []string{
|
||||
"http://up-z1.qiniu.com",
|
||||
"http://upload-z1.qiniu.com",
|
||||
"-H up-z1.qiniu.com http://106.38.227.27",
|
||||
},
|
||||
},
|
||||
// z2 华南机房:
|
||||
{
|
||||
IoHost: "http://iovip-z2.qbox.me",
|
||||
UpHosts: []string{
|
||||
"http://up-z2.qiniu.com",
|
||||
"http://upload-z2.qiniu.com",
|
||||
},
|
||||
},
|
||||
// na0 北美机房:
|
||||
{
|
||||
IoHost: "http://iovip-na0.qbox.me",
|
||||
UpHosts: []string{
|
||||
"http://up-na0.qiniu.com",
|
||||
"http://upload-na0.qiniu.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRsHost = "http://rs.qbox.me"
|
||||
defaultRsfHost = "http://rsf.qbox.me"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type Config struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
RSHost string
|
||||
RSFHost string
|
||||
APIHost string
|
||||
Scheme string
|
||||
IoHost string
|
||||
UpHosts []string
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type Client struct {
|
||||
rpc.Client
|
||||
mac *qbox.Mac
|
||||
Config
|
||||
|
||||
apiCli *api.Client
|
||||
}
|
||||
|
||||
func New(zone int, cfg *Config) (p *Client) {
|
||||
|
||||
p = new(Client)
|
||||
if cfg != nil {
|
||||
p.Config = *cfg
|
||||
}
|
||||
|
||||
p.mac = qbox.NewMac(p.AccessKey, p.SecretKey)
|
||||
p.Client = rpc.Client{qbox.NewClient(p.mac, p.Transport)}
|
||||
|
||||
if p.RSHost == "" {
|
||||
p.RSHost = defaultRsHost
|
||||
}
|
||||
if p.RSFHost == "" {
|
||||
p.RSFHost = defaultRsfHost
|
||||
}
|
||||
if p.Scheme != "https" {
|
||||
p.Scheme = "http"
|
||||
}
|
||||
if p.APIHost == "" {
|
||||
p.APIHost = api.DefaultApiHost
|
||||
}
|
||||
p.apiCli = api.NewClient(p.APIHost, p.Scheme)
|
||||
|
||||
if zone < 0 || zone >= len(zones) {
|
||||
return
|
||||
}
|
||||
if len(p.UpHosts) == 0 {
|
||||
p.UpHosts = zones[zone].UpHosts
|
||||
}
|
||||
if p.IoHost == "" {
|
||||
p.IoHost = zones[zone].IoHost
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewWithoutZone(cfg *Config) (p *Client) {
|
||||
return New(-1, cfg)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 设置全局默认的 ACCESS_KEY, SECRET_KEY 变量。
|
||||
//
|
||||
func SetMac(accessKey, secretKey string) {
|
||||
|
||||
conf.ACCESS_KEY, conf.SECRET_KEY = accessKey, secretKey
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 设置使用这个SDK的应用程序名。userApp 必须满足 [A-Za-z0-9_\ \-\.]*
|
||||
//
|
||||
func SetAppName(userApp string) error {
|
||||
|
||||
return conf.SetAppName(userApp)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package kodo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
key = "aa"
|
||||
keyFetch = "afetch"
|
||||
newkey1 = "bbbb"
|
||||
newkey2 = "cccc"
|
||||
fetchURL = "http://www-static.u.qiniucdn.com/public/v1645/img/css-sprite.png"
|
||||
bucketName string
|
||||
domain string
|
||||
client *Client
|
||||
bucket = newBucket()
|
||||
|
||||
QINIU_KODO_TEST string
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
if skipTest() {
|
||||
return
|
||||
}
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
key += strconv.Itoa(rand.Int())
|
||||
keyFetch += strconv.Itoa(rand.Int())
|
||||
newkey1 += strconv.Itoa(rand.Int())
|
||||
newkey2 += strconv.Itoa(rand.Int())
|
||||
bucket.BatchDelete(nil, key, keyFetch, newkey1, newkey2)
|
||||
}
|
||||
|
||||
func newBucket() (bucket Bucket) {
|
||||
|
||||
QINIU_KODO_TEST = os.Getenv("QINIU_KODO_TEST")
|
||||
if skipTest() {
|
||||
println("[INFO] QINIU_KODO_TEST: skipping to test qiniupkg.com/api.v7")
|
||||
return
|
||||
}
|
||||
|
||||
ak := os.Getenv("QINIU_ACCESS_KEY")
|
||||
sk := os.Getenv("QINIU_SECRET_KEY")
|
||||
if ak == "" || sk == "" {
|
||||
panic("require ACCESS_KEY & SECRET_KEY")
|
||||
}
|
||||
SetMac(ak, sk)
|
||||
|
||||
bucketName = os.Getenv("QINIU_TEST_BUCKET")
|
||||
domain = os.Getenv("QINIU_TEST_DOMAIN")
|
||||
if bucketName == "" || domain == "" {
|
||||
panic("require test env")
|
||||
}
|
||||
client = NewWithoutZone(nil)
|
||||
|
||||
return client.Bucket(bucketName)
|
||||
}
|
||||
|
||||
func skipTest() bool {
|
||||
|
||||
return QINIU_KODO_TEST == ""
|
||||
}
|
||||
|
||||
func upFile(localFile, key string) error {
|
||||
|
||||
return bucket.PutFile(nil, nil, key, localFile, nil)
|
||||
}
|
||||
|
||||
func TestFetch(t *testing.T) {
|
||||
|
||||
if skipTest() {
|
||||
return
|
||||
}
|
||||
|
||||
err := bucket.Fetch(nil, keyFetch, fetchURL)
|
||||
if err != nil {
|
||||
t.Fatal("bucket.Fetch failed:", err)
|
||||
}
|
||||
|
||||
entry, err := bucket.Stat(nil, keyFetch)
|
||||
if err != nil || entry.MimeType != "image/png" {
|
||||
t.Fatal("bucket.Fetch: Stat failed -", err, "entry:", entry)
|
||||
}
|
||||
fmt.Println(entry)
|
||||
}
|
||||
|
||||
func TestEntry(t *testing.T) {
|
||||
|
||||
if skipTest() {
|
||||
return
|
||||
}
|
||||
|
||||
//上传一个文件用用于测试
|
||||
err := upFile("doc.go", key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer bucket.Delete(nil, key)
|
||||
|
||||
einfo, err := bucket.Stat(nil, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mime := "text/plain"
|
||||
err = bucket.ChangeMime(nil, key, mime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
einfo, err = bucket.Stat(nil, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if einfo.MimeType != mime {
|
||||
t.Fatal("mime type did not change")
|
||||
}
|
||||
|
||||
err = bucket.Copy(nil, key, newkey1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enewinfo, err := bucket.Stat(nil, newkey1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if einfo.Hash != enewinfo.Hash {
|
||||
t.Fatal("invalid entryinfo:", einfo, enewinfo)
|
||||
}
|
||||
err = bucket.Move(nil, newkey1, newkey2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enewinfo2, err := bucket.Stat(nil, newkey2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if enewinfo.Hash != enewinfo2.Hash {
|
||||
t.Fatal("invalid entryinfo:", enewinfo, enewinfo2)
|
||||
}
|
||||
|
||||
err = bucket.Delete(nil, newkey2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package kodo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"qiniupkg.com/api.v7/api"
|
||||
"qiniupkg.com/api.v7/auth/qbox"
|
||||
"qiniupkg.com/x/url.v7"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 根据空间(Bucket)的域名,以及文件的 key,获得 baseUrl。
|
||||
// 如果空间是 public 的,那么通过 baseUrl 可以直接下载文件内容。
|
||||
// 如果空间是 private 的,那么需要对 baseUrl 进行私有签名得到一个临时有效的 privateUrl 进行下载。
|
||||
//
|
||||
func MakeBaseUrl(domain, key string) (baseUrl string) {
|
||||
|
||||
return "http://" + domain + "/" + url.Escape(key)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type GetPolicy struct {
|
||||
Expires uint32
|
||||
}
|
||||
|
||||
func (p *Client) MakePrivateUrl(baseUrl string, policy *GetPolicy) (privateUrl string) {
|
||||
|
||||
var expires int64
|
||||
if policy == nil || policy.Expires == 0 {
|
||||
expires = 3600
|
||||
} else {
|
||||
expires = int64(policy.Expires)
|
||||
}
|
||||
deadline := time.Now().Unix() + expires
|
||||
|
||||
if strings.Contains(baseUrl, "?") {
|
||||
baseUrl += "&e="
|
||||
} else {
|
||||
baseUrl += "?e="
|
||||
}
|
||||
baseUrl += strconv.FormatInt(deadline, 10)
|
||||
|
||||
token := qbox.Sign(p.mac, []byte(baseUrl))
|
||||
return baseUrl + "&token=" + token
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
type PutPolicy struct {
|
||||
Scope string `json:"scope"`
|
||||
Expires uint32 `json:"deadline"` // 截止时间(以秒为单位)
|
||||
InsertOnly uint16 `json:"insertOnly,omitempty"` // 若非0, 即使Scope为 Bucket:Key 的形式也是insert only
|
||||
DetectMime uint8 `json:"detectMime,omitempty"` // 若非0, 则服务端根据内容自动确定 MimeType
|
||||
CallbackFetchKey uint8 `json:"callbackFetchKey,omitempty"`
|
||||
FsizeLimit int64 `json:"fsizeLimit,omitempty"`
|
||||
MimeLimit string `json:"mimeLimit,omitempty"`
|
||||
SaveKey string `json:"saveKey,omitempty"`
|
||||
CallbackUrl string `json:"callbackUrl,omitempty"`
|
||||
CallbackHost string `json:"callbackHost,omitempty"`
|
||||
CallbackBody string `json:"callbackBody,omitempty"`
|
||||
CallbackBodyType string `json:"callbackBodyType,omitempty"`
|
||||
ReturnUrl string `json:"returnUrl,omitempty"`
|
||||
ReturnBody string `json:"returnBody,omitempty"`
|
||||
PersistentOps string `json:"persistentOps,omitempty"`
|
||||
PersistentNotifyUrl string `json:"persistentNotifyUrl,omitempty"`
|
||||
PersistentPipeline string `json:"persistentPipeline,omitempty"`
|
||||
AsyncOps string `json:"asyncOps,omitempty"`
|
||||
EndUser string `json:"endUser,omitempty"`
|
||||
Checksum string `json:"checksum,omitempty"` // 格式:<HashName>:<HexHashValue>,目前支持 MD5/SHA1。
|
||||
UpHosts []string `json:"uphosts,omitempty"`
|
||||
DeleteAfterDays int `json:"deleteAfterDays,omitempty"`
|
||||
}
|
||||
|
||||
func (p *Client) MakeUptoken(policy *PutPolicy) string {
|
||||
token, err := p.MakeUptokenWithSafe(policy)
|
||||
if err != nil {
|
||||
fmt.Errorf("makeuptoken failed: policy: %+v, error: %+v", policy, err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func (p *Client) MakeUptokenWithSafe(policy *PutPolicy) (token string, err error) {
|
||||
var rr = *policy
|
||||
if len(rr.UpHosts) == 0 {
|
||||
bucketName := getBucketNameFromPutPolicy(policy)
|
||||
bucketInfo, err1 := p.GetBucketInfo(bucketName)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
return
|
||||
}
|
||||
rr.UpHosts = bucketInfo.UpHosts
|
||||
}
|
||||
if rr.Expires == 0 {
|
||||
rr.Expires = 3600
|
||||
}
|
||||
rr.Expires += uint32(time.Now().Unix())
|
||||
b, _ := json.Marshal(&rr)
|
||||
token = qbox.SignWithData(p.mac, b)
|
||||
return
|
||||
}
|
||||
|
||||
func getBucketNameFromPutPolicy(policy *PutPolicy) (bucketName string) {
|
||||
scope := policy.Scope
|
||||
bucketName = strings.Split(scope, ":")[0]
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Client) GetBucketInfo(bucketName string) (bucketInfo api.BucketInfo, err error) {
|
||||
return p.apiCli.GetBucketInfo(p.mac.AccessKey, bucketName)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package kodo
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
if skipTest() {
|
||||
return
|
||||
}
|
||||
|
||||
// 删除 可能存在的 key
|
||||
bucket.BatchDelete(nil, key)
|
||||
}
|
||||
|
||||
func TestGetPrivateUrl(t *testing.T) {
|
||||
|
||||
if skipTest() {
|
||||
return
|
||||
}
|
||||
|
||||
// 上传一个文件用用于测试
|
||||
err := upFile("token.go", key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer bucket.Delete(nil, key)
|
||||
|
||||
baseUrl := MakeBaseUrl(domain, key)
|
||||
privateUrl := client.MakePrivateUrl(baseUrl, nil)
|
||||
|
||||
resp, err := http.Get(privateUrl)
|
||||
if err != nil {
|
||||
t.Fatal("http.Get failed:", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
h := sha1.New()
|
||||
io.Copy(h, resp.Body)
|
||||
etagExpected := base64.URLEncoding.EncodeToString(h.Sum([]byte{'\x16'}))
|
||||
|
||||
etag := resp.Header.Get("Etag")
|
||||
if etag[1:len(etag)-1] != etagExpected {
|
||||
t.Fatal("http.Get etag failed:", etag, etagExpected)
|
||||
}
|
||||
}
|
||||
|
||||
func testClient_MakeUptokenBucket(t *testing.T) {
|
||||
c := New(0, nil)
|
||||
token := c.MakeUptoken(&PutPolicy{
|
||||
Scope: "gosdk",
|
||||
Expires: 3600,
|
||||
})
|
||||
if token == "" {
|
||||
t.Fatal("nil token")
|
||||
}
|
||||
|
||||
token, err := c.MakeUptokenWithSafe(&PutPolicy{
|
||||
Scope: "NotExistBucket",
|
||||
Expires: 3600,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("make up token fail")
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package kodo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"qiniupkg.com/api.v7/kodocli"
|
||||
"qiniupkg.com/x/rpc.v7"
|
||||
|
||||
. "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type PutExtra kodocli.PutExtra
|
||||
type RputExtra kodocli.RputExtra
|
||||
type PutRet kodocli.PutRet
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func (p Bucket) makeUptoken(key string) string {
|
||||
|
||||
policy := &PutPolicy{
|
||||
Scope: p.Name + ":" + key,
|
||||
Expires: 3600,
|
||||
UpHosts: p.UpHosts,
|
||||
}
|
||||
return p.Conn.MakeUptoken(policy)
|
||||
}
|
||||
|
||||
func (p Bucket) makeUptokenWithoutKey() string {
|
||||
|
||||
policy := &PutPolicy{
|
||||
Scope: p.Name,
|
||||
Expires: 3600,
|
||||
UpHosts: p.UpHosts,
|
||||
}
|
||||
return p.Conn.MakeUptoken(policy)
|
||||
}
|
||||
|
||||
func (p Bucket) makeUploader() kodocli.Uploader {
|
||||
|
||||
c := &http.Client{Transport: p.Conn.Transport}
|
||||
return kodocli.Uploader{Conn: rpc.Client{c}, UpHosts: p.UpHosts, ApiCli: p.Conn.apiCli}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 上传一个文件。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。返回的是 PutRet 结构。可选,可以传 nil 表示不感兴趣。
|
||||
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
|
||||
// data 是文件内容的访问接口(io.Reader)。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) Put(
|
||||
ctx Context, ret interface{}, key string, data io.Reader, size int64, extra *PutExtra) error {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptoken(key)
|
||||
return uploader.Put(ctx, ret, uptoken, key, data, size, (*kodocli.PutExtra)(extra))
|
||||
}
|
||||
|
||||
// 上传一个文件。自动以文件的 hash 作为文件的访问路径(key)。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。返回的是 PutRet 结构。可选,可以传 nil 表示不感兴趣。
|
||||
// data 是文件内容的访问接口(io.Reader)。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) PutWithoutKey(
|
||||
ctx Context, ret interface{}, data io.Reader, size int64, extra *PutExtra) error {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptokenWithoutKey()
|
||||
return uploader.PutWithoutKey(ctx, ret, uptoken, data, size, (*kodocli.PutExtra)(extra))
|
||||
}
|
||||
|
||||
// 上传一个文件。
|
||||
// 和 Put 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.Reader 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。返回的是 PutRet 结构。可选,可以传 nil 表示不感兴趣。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) PutFile(
|
||||
ctx Context, ret interface{}, key, localFile string, extra *PutExtra) (err error) {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptoken(key)
|
||||
return uploader.PutFile(ctx, ret, uptoken, key, localFile, (*kodocli.PutExtra)(extra))
|
||||
}
|
||||
|
||||
// 上传一个文件。自动以文件的 hash 作为文件的访问路径(key)。
|
||||
// 和 PutWithoutKey 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.Reader 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。返回的是 PutRet 结构。可选,可以传 nil 表示不感兴趣。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) PutFileWithoutKey(
|
||||
ctx Context, ret interface{}, localFile string, extra *PutExtra) (err error) {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptokenWithoutKey()
|
||||
return uploader.PutFileWithoutKey(ctx, ret, uptoken, localFile, (*kodocli.PutExtra)(extra))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
|
||||
// data 是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) Rput(
|
||||
ctx Context, ret interface{}, key string, data io.ReaderAt, size int64, extra *RputExtra) error {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptoken(key)
|
||||
return uploader.Rput(ctx, ret, uptoken, key, data, size, (*kodocli.RputExtra)(extra))
|
||||
}
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。自动以文件的 hash 作为文件的访问路径(key)。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// data 是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) RputWithoutKey(
|
||||
ctx Context, ret interface{}, data io.ReaderAt, size int64, extra *RputExtra) error {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptokenWithoutKey()
|
||||
return uploader.RputWithoutKey(ctx, ret, uptoken, data, size, (*kodocli.RputExtra)(extra))
|
||||
}
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。
|
||||
// 和 Rput 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) RputFile(
|
||||
ctx Context, ret interface{}, key, localFile string, extra *RputExtra) (err error) {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptoken(key)
|
||||
return uploader.RputFile(ctx, ret, uptoken, key, localFile, (*kodocli.RputExtra)(extra))
|
||||
}
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。自动以文件的 hash 作为文件的访问路径(key)。
|
||||
// 和 RputWithoutKey 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Bucket) RputFileWithoutKey(
|
||||
ctx Context, ret interface{}, localFile string, extra *RputExtra) (err error) {
|
||||
|
||||
uploader := p.makeUploader()
|
||||
uptoken := p.makeUptokenWithoutKey()
|
||||
return uploader.RputFileWithoutKey(ctx, ret, uptoken, localFile, (*kodocli.RputExtra)(extra))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
包 qiniupkg.com/api.v7/kodocli 提供了在客户端调用七牛云存储部分服务的能力
|
||||
|
||||
客户端,严谨说是非可信环境,主要是指在用户端执行的环境,比如:Android/iOS 设备、Windows/Mac/Linux 桌面环境、也包括浏览器(如果浏览器能够执行 Go 语言代码的话)。
|
||||
|
||||
注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,会影响您的数据安全。
|
||||
|
||||
第一个问题是如何上传文件。因为是在非可信环境,所以我们首先是要授予它有上传文件的能力。答案是给它颁发上传凭证。假设服务端也是 Go 语言,那么:
|
||||
|
||||
import "qiniupkg.com/api.v7/kodo"
|
||||
|
||||
kodo.SetMac("your-access-key", "your-secret-key") // 配置 AccessKey/SecretKey
|
||||
|
||||
zone := 0
|
||||
c := kodo.New(zone, nil) // 创建一个 Client 对象
|
||||
|
||||
bucket := "your-bucket-name"
|
||||
key := "foo/bar.jpg"
|
||||
policy := &kodo.PutPolicy{
|
||||
Scope: bucket + ":" + key, // 上传文件的限制条件,这里限制只能上传一个名为 "foo/bar.jpg" 的文件
|
||||
Expires: 3600, // 这是限制上传凭证(uptoken)的过期时长,3600 是一小时
|
||||
...
|
||||
}
|
||||
uptoken := c.MakeUptoken(policy) // 生成上传凭证
|
||||
|
||||
生成上传凭证之后,通过某种方式将 uptoken 发送到客户端。这样客户端就可以上传文件了:
|
||||
|
||||
zone := 0
|
||||
uploader := kodocli.NewUploader(zone, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
key := "foo/bar.jpg"
|
||||
localFile := "/your/local/image/file.jpg"
|
||||
err := uploader.PutFile(ctx, nil, uptoken, key, localFile, nil)
|
||||
if err != nil {
|
||||
... // 上传文件失败处理
|
||||
return
|
||||
}
|
||||
|
||||
注意,如果客户端上传的 key 不是 uptoken 所要求的 "foo/bar.jpg",那么上传就会被拒绝。
|
||||
|
||||
如果我们希望一个 uptoken 可以上传多个文件,那么服务端颁发 uptoken 的代码需要调整下:
|
||||
|
||||
bucket := "your-bucket-name"
|
||||
policy := &kodo.PutPolicy{
|
||||
Scope: bucket, // 上传文件的限制条件,这里是只限制了要上传到 "your-bucket-name" 空间
|
||||
Expires: 3600, // 这是限制上传凭证(uptoken)的过期时长,3600 是一小时
|
||||
...
|
||||
}
|
||||
uptoken := c.MakeUptoken(policy)
|
||||
|
||||
颁发这样的 uptoken 给客户端,客户端就可以用它上传任意名字(key)的文件,前提是服务器上还没有同名的文件。
|
||||
|
||||
特别需要注意的是,这种情况下服务端会拒绝已经有同名文件的上传请求,而不是覆盖服务器上已有的文件。这是出于数据安全的考虑。我们并不非常推荐 uptoken 复用的做法,除了注意文件名可能冲突外,还需要注意 uptoken 是有时效的,过期就需要重新向服务器申请新的上传凭证。
|
||||
|
||||
搞定文件的上传后,第二个问题是如何下载文件。如果是公开文件,我们只需要:
|
||||
|
||||
import "net/http"
|
||||
|
||||
domain := "domain-of-your-bucket.com" // 您的空间绑定的域名,这个可以在七牛的Portal中查到
|
||||
baseUrl := kodocli.MakeBaseUrl(domain, "foo/bar.jpg") // 得到下载 url
|
||||
resp, err := http.Get(baseUrl)
|
||||
...
|
||||
|
||||
但是对于私有空间,事情要复杂一些,需要客户端向您的业务服务器申请下载该文件。业务服务器确认该用户有权访问,则返回一个临时有效的 privateUrl。具体如何生成 privateUrl,可以看服务端 SDK 相关的文档介绍。
|
||||
*/
|
||||
package kodocli
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package kodocli
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"qiniupkg.com/api.v7/api"
|
||||
"qiniupkg.com/api.v7/conf"
|
||||
"qiniupkg.com/x/rpc.v7"
|
||||
"qiniupkg.com/x/url.v7"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type zoneConfig struct {
|
||||
UpHosts []string
|
||||
}
|
||||
|
||||
var zones = []zoneConfig{
|
||||
// z0:
|
||||
{
|
||||
UpHosts: []string{
|
||||
"http://upload.qiniu.com",
|
||||
"http://up.qiniu.com",
|
||||
"-H up.qiniu.com http://183.136.139.16",
|
||||
},
|
||||
},
|
||||
// z1:
|
||||
{
|
||||
UpHosts: []string{
|
||||
"http://upload-z1.qiniu.com",
|
||||
"http://up-z1.qiniu.com",
|
||||
"-H up-z1.qiniu.com http://106.38.227.27",
|
||||
},
|
||||
},
|
||||
// z2 华南机房:
|
||||
{
|
||||
UpHosts: []string{
|
||||
"http://up-z2.qiniu.com",
|
||||
"http://upload-z2.qiniu.com",
|
||||
},
|
||||
},
|
||||
// na0 北美机房:
|
||||
{
|
||||
UpHosts: []string{
|
||||
"http://up-na0.qiniu.com",
|
||||
"http://upload-na0.qiniu.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type UploadConfig struct {
|
||||
UpHosts []string
|
||||
Transport http.RoundTripper
|
||||
|
||||
APIHost string
|
||||
Scheme string
|
||||
}
|
||||
|
||||
type Uploader struct {
|
||||
Conn rpc.Client
|
||||
UpHosts []string
|
||||
ApiCli *api.Client
|
||||
}
|
||||
|
||||
func NewUploader(zone int, cfg *UploadConfig) (p Uploader) {
|
||||
|
||||
var uc UploadConfig
|
||||
if cfg != nil {
|
||||
uc = *cfg
|
||||
}
|
||||
if uc.Scheme != "https" {
|
||||
uc.Scheme = "http"
|
||||
}
|
||||
if uc.APIHost == "" {
|
||||
uc.APIHost = api.DefaultApiHost
|
||||
}
|
||||
if len(uc.UpHosts) == 0 {
|
||||
if zone > 0 && zone < len(zones) {
|
||||
uc.UpHosts = zones[zone].UpHosts
|
||||
}
|
||||
}
|
||||
|
||||
p.UpHosts = uc.UpHosts
|
||||
p.Conn.Client = &http.Client{Transport: uc.Transport}
|
||||
p.ApiCli = api.NewClient(uc.APIHost, uc.Scheme)
|
||||
return
|
||||
}
|
||||
|
||||
func NewUploaderWithoutZone(cfg *UploadConfig) (p Uploader) {
|
||||
return NewUploader(-1, cfg)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 根据空间(Bucket)的域名,以及文件的 key,获得 baseUrl。
|
||||
// 如果空间是 public 的,那么通过 baseUrl 可以直接下载文件内容。
|
||||
// 如果空间是 private 的,那么需要对 baseUrl 进行私有签名得到一个临时有效的 privateUrl 进行下载。
|
||||
//
|
||||
func MakeBaseUrl(domain, key string) (baseUrl string) {
|
||||
return "http://" + domain + "/" + url.Escape(key)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 设置使用这个SDK的应用程序名。userApp 必须满足 [A-Za-z0-9_\ \-\.]*
|
||||
//
|
||||
func SetAppName(userApp string) error {
|
||||
|
||||
return conf.SetAppName(userApp)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
package kodocli
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"qiniupkg.com/x/xlog.v7"
|
||||
|
||||
. "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
var (
|
||||
ErrInvalidPutProgress = errors.New("invalid put progress")
|
||||
ErrPutFailed = errors.New("resumable put failed")
|
||||
ErrUnmatchedChecksum = errors.New("unmatched checksum")
|
||||
ErrBadToken = errors.New("invalid token")
|
||||
)
|
||||
|
||||
const (
|
||||
InvalidCtx = 701 // UP: 无效的上下文(bput),可能情况:Ctx非法或者已经被淘汰(太久未使用)
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWorkers = 4
|
||||
defaultChunkSize = 256 * 1024 // 256k
|
||||
defaultTryTimes = 3
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
TaskQsize int // 可选。任务队列大小。为 0 表示取 Workers * 4。
|
||||
Workers int // 并行 Goroutine 数目。
|
||||
ChunkSize int // 默认的Chunk大小,不设定则为256k
|
||||
TryTimes int // 默认的尝试次数,不设定则为3
|
||||
}
|
||||
|
||||
var settings = Settings{
|
||||
TaskQsize: defaultWorkers * 4,
|
||||
Workers: defaultWorkers,
|
||||
ChunkSize: defaultChunkSize,
|
||||
TryTimes: defaultTryTimes,
|
||||
}
|
||||
|
||||
func SetSettings(v *Settings) {
|
||||
|
||||
settings = *v
|
||||
if settings.Workers == 0 {
|
||||
settings.Workers = defaultWorkers
|
||||
}
|
||||
if settings.TaskQsize == 0 {
|
||||
settings.TaskQsize = settings.Workers * 4
|
||||
}
|
||||
if settings.ChunkSize == 0 {
|
||||
settings.ChunkSize = defaultChunkSize
|
||||
}
|
||||
if settings.TryTimes == 0 {
|
||||
settings.TryTimes = defaultTryTimes
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
var tasks chan func()
|
||||
|
||||
func worker(tasks chan func()) {
|
||||
for {
|
||||
task := <-tasks
|
||||
task()
|
||||
}
|
||||
}
|
||||
|
||||
func initWorkers() {
|
||||
|
||||
tasks = make(chan func(), settings.TaskQsize)
|
||||
for i := 0; i < settings.Workers; i++ {
|
||||
go worker(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
func notifyNil(blkIdx int, blkSize int, ret *BlkputRet) {}
|
||||
func notifyErrNil(blkIdx int, blkSize int, err error) {}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
const (
|
||||
blockBits = 22
|
||||
blockMask = (1 << blockBits) - 1
|
||||
)
|
||||
|
||||
func BlockCount(fsize int64) int {
|
||||
return int((fsize + blockMask) >> blockBits)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type BlkputRet struct {
|
||||
Ctx string `json:"ctx"`
|
||||
Checksum string `json:"checksum"`
|
||||
Crc32 uint32 `json:"crc32"`
|
||||
Offset uint32 `json:"offset"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type RputExtra struct {
|
||||
Params map[string]string // 可选。用户自定义参数,以"x:"开头 否则忽略
|
||||
MimeType string // 可选。
|
||||
ChunkSize int // 可选。每次上传的Chunk大小
|
||||
TryTimes int // 可选。尝试次数
|
||||
Progresses []BlkputRet // 可选。上传进度
|
||||
Notify func(blkIdx int, blkSize int, ret *BlkputRet) // 可选。进度提示(注意多个block是并行传输的)
|
||||
NotifyErr func(blkIdx int, blkSize int, err error)
|
||||
}
|
||||
|
||||
var once sync.Once
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type Policy struct {
|
||||
Scope string `json:"scope"`
|
||||
UpHosts []string `json:"uphosts"`
|
||||
}
|
||||
|
||||
func unmarshal(uptoken string, uptokenPolicy *Policy) (err error) {
|
||||
parts := strings.Split(uptoken, ":")
|
||||
if len(parts) != 3 {
|
||||
err = ErrBadToken
|
||||
return
|
||||
}
|
||||
b, err := base64.URLEncoding.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, uptokenPolicy)
|
||||
}
|
||||
|
||||
func (p Uploader) getUpHostFromToken(uptoken string) (uphosts []string, err error) {
|
||||
if len(p.UpHosts) != 0 {
|
||||
uphosts = p.UpHosts
|
||||
return
|
||||
}
|
||||
ak := strings.Split(uptoken, ":")[0]
|
||||
uptokenPolicy := Policy{}
|
||||
err = unmarshal(uptoken, &uptokenPolicy)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(uptokenPolicy.UpHosts) == 0 {
|
||||
bucketName := strings.Split(uptokenPolicy.Scope, ":")[0]
|
||||
bucketInfo, err1 := p.ApiCli.GetBucketInfo(ak, bucketName)
|
||||
if err1 != nil {
|
||||
err = err1
|
||||
return
|
||||
}
|
||||
uphosts = bucketInfo.UpHosts
|
||||
} else {
|
||||
uphosts = uptokenPolicy.UpHosts
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
|
||||
// f 是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) Rput(
|
||||
ctx Context, ret interface{}, uptoken string,
|
||||
key string, f io.ReaderAt, fsize int64, extra *RputExtra) error {
|
||||
|
||||
return p.rput(ctx, ret, uptoken, key, true, f, fsize, extra)
|
||||
}
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。文件的访问路径(key)自动生成。
|
||||
// 如果 uptoken 中设置了 SaveKey,那么按 SaveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// f 是文件内容的访问接口。考虑到需要支持分块上传和断点续传,要的是 io.ReaderAt 接口,而不是 io.Reader。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) RputWithoutKey(
|
||||
ctx Context, ret interface{}, uptoken string, f io.ReaderAt, fsize int64, extra *RputExtra) error {
|
||||
|
||||
return p.rput(ctx, ret, uptoken, "", false, f, fsize, extra)
|
||||
}
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。
|
||||
// 和 Rput 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) RputFile(
|
||||
ctx Context, ret interface{}, uptoken, key, localFile string, extra *RputExtra) (err error) {
|
||||
|
||||
return p.rputFile(ctx, ret, uptoken, key, true, localFile, extra)
|
||||
}
|
||||
|
||||
// 上传一个文件,支持断点续传和分块上传。文件的访问路径(key)自动生成。
|
||||
// 如果 uptoken 中设置了 SaveKey,那么按 SaveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。
|
||||
// 和 RputWithoutKey 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.ReaderAt 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 RputExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) RputFileWithoutKey(
|
||||
ctx Context, ret interface{}, uptoken, localFile string, extra *RputExtra) (err error) {
|
||||
|
||||
return p.rputFile(ctx, ret, uptoken, "", false, localFile, extra)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func (p Uploader) rput(
|
||||
ctx Context, ret interface{}, uptoken string,
|
||||
key string, hasKey bool, f io.ReaderAt, fsize int64, extra *RputExtra) error {
|
||||
|
||||
once.Do(initWorkers)
|
||||
|
||||
log := xlog.NewWith(ctx)
|
||||
blockCnt := BlockCount(fsize)
|
||||
|
||||
if extra == nil {
|
||||
extra = new(RputExtra)
|
||||
}
|
||||
if extra.Progresses == nil {
|
||||
extra.Progresses = make([]BlkputRet, blockCnt)
|
||||
} else if len(extra.Progresses) != blockCnt {
|
||||
return ErrInvalidPutProgress
|
||||
}
|
||||
|
||||
if extra.ChunkSize == 0 {
|
||||
extra.ChunkSize = settings.ChunkSize
|
||||
}
|
||||
if extra.TryTimes == 0 {
|
||||
extra.TryTimes = settings.TryTimes
|
||||
}
|
||||
if extra.Notify == nil {
|
||||
extra.Notify = notifyNil
|
||||
}
|
||||
if extra.NotifyErr == nil {
|
||||
extra.NotifyErr = notifyErrNil
|
||||
}
|
||||
uphosts, err := p.getUpHostFromToken(uptoken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(blockCnt)
|
||||
|
||||
last := blockCnt - 1
|
||||
blkSize := 1 << blockBits
|
||||
nfails := 0
|
||||
p.Conn.Client = newUptokenClient(uptoken, p.Conn.Transport)
|
||||
|
||||
for i := 0; i < blockCnt; i++ {
|
||||
blkIdx := i
|
||||
blkSize1 := blkSize
|
||||
if i == last {
|
||||
offbase := int64(blkIdx) << blockBits
|
||||
blkSize1 = int(fsize - offbase)
|
||||
}
|
||||
task := func() {
|
||||
defer wg.Done()
|
||||
tryTimes := extra.TryTimes
|
||||
lzRetry:
|
||||
err := p.resumableBput(ctx, uphosts, &extra.Progresses[blkIdx], f, blkIdx, blkSize1, extra)
|
||||
if err != nil {
|
||||
if tryTimes > 1 {
|
||||
tryTimes--
|
||||
log.Info("resumable.Put retrying ...", blkIdx, "reason:", err)
|
||||
goto lzRetry
|
||||
}
|
||||
log.Warn("resumable.Put", blkIdx, "failed:", err)
|
||||
extra.NotifyErr(blkIdx, blkSize1, err)
|
||||
nfails++
|
||||
}
|
||||
}
|
||||
tasks <- task
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
if nfails != 0 {
|
||||
return ErrPutFailed
|
||||
}
|
||||
|
||||
return p.mkfile(ctx, uphosts, ret, key, hasKey, fsize, extra)
|
||||
}
|
||||
|
||||
func (p Uploader) rputFile(
|
||||
ctx Context, ret interface{}, uptoken string,
|
||||
key string, hasKey bool, localFile string, extra *RputExtra) (err error) {
|
||||
|
||||
f, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return p.rput(ctx, ret, uptoken, key, hasKey, f, fi.Size(), extra)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package kodocli
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"qiniupkg.com/x/bytes.v7"
|
||||
"qiniupkg.com/x/rpc.v7"
|
||||
"qiniupkg.com/x/xlog.v7"
|
||||
|
||||
. "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type uptokenTransport struct {
|
||||
token string
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *uptokenTransport) NestedObject() interface{} {
|
||||
return t.Transport
|
||||
}
|
||||
|
||||
func (t *uptokenTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
req.Header.Set("Authorization", t.token)
|
||||
return t.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func newUptokenTransport(token string, transport http.RoundTripper) *uptokenTransport {
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
return &uptokenTransport{"UpToken " + token, transport}
|
||||
}
|
||||
|
||||
func newUptokenClient(token string, transport http.RoundTripper) *http.Client {
|
||||
t := newUptokenTransport(token, transport)
|
||||
return &http.Client{Transport: t}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func (p Uploader) mkblk(
|
||||
ctx Context, uphosts []string, ret *BlkputRet, blockSize int, body io.Reader, size int) error {
|
||||
|
||||
url := uphosts[0] + "/mkblk/" + strconv.Itoa(blockSize)
|
||||
return p.Conn.CallWith(ctx, ret, "POST", url, "application/octet-stream", body, size)
|
||||
}
|
||||
|
||||
func (p Uploader) bput(
|
||||
ctx Context, ret *BlkputRet, body io.Reader, size int) error {
|
||||
|
||||
url := ret.Host + "/bput/" + ret.Ctx + "/" + strconv.FormatUint(uint64(ret.Offset), 10)
|
||||
return p.Conn.CallWith(ctx, ret, "POST", url, "application/octet-stream", body, size)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func (p Uploader) resumableBput(
|
||||
ctx Context, uphosts []string, ret *BlkputRet, f io.ReaderAt, blkIdx, blkSize int, extra *RputExtra) (err error) {
|
||||
|
||||
log := xlog.NewWith(ctx)
|
||||
h := crc32.NewIEEE()
|
||||
offbase := int64(blkIdx) << blockBits
|
||||
chunkSize := extra.ChunkSize
|
||||
|
||||
var bodyLength int
|
||||
|
||||
if ret.Ctx == "" {
|
||||
|
||||
if chunkSize < blkSize {
|
||||
bodyLength = chunkSize
|
||||
} else {
|
||||
bodyLength = blkSize
|
||||
}
|
||||
|
||||
body1 := io.NewSectionReader(f, offbase, int64(bodyLength))
|
||||
body := io.TeeReader(body1, h)
|
||||
|
||||
err = p.mkblk(ctx, uphosts, ret, blkSize, body, bodyLength)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ret.Crc32 != h.Sum32() || int(ret.Offset) != bodyLength {
|
||||
err = ErrUnmatchedChecksum
|
||||
return
|
||||
}
|
||||
extra.Notify(blkIdx, blkSize, ret)
|
||||
}
|
||||
|
||||
for int(ret.Offset) < blkSize {
|
||||
|
||||
if chunkSize < blkSize-int(ret.Offset) {
|
||||
bodyLength = chunkSize
|
||||
} else {
|
||||
bodyLength = blkSize - int(ret.Offset)
|
||||
}
|
||||
|
||||
tryTimes := extra.TryTimes
|
||||
|
||||
lzRetry:
|
||||
h.Reset()
|
||||
body1 := io.NewSectionReader(f, offbase+int64(ret.Offset), int64(bodyLength))
|
||||
body := io.TeeReader(body1, h)
|
||||
|
||||
err = p.bput(ctx, ret, body, bodyLength)
|
||||
if err == nil {
|
||||
if ret.Crc32 == h.Sum32() {
|
||||
extra.Notify(blkIdx, blkSize, ret)
|
||||
continue
|
||||
}
|
||||
log.Warn("ResumableBlockput: invalid checksum, retry")
|
||||
err = ErrUnmatchedChecksum
|
||||
} else {
|
||||
if ei, ok := err.(*rpc.ErrorInfo); ok && ei.Code == InvalidCtx {
|
||||
ret.Ctx = "" // reset
|
||||
log.Warn("ResumableBlockput: invalid ctx, please retry")
|
||||
return
|
||||
}
|
||||
log.Warn("ResumableBlockput: bput failed -", err)
|
||||
}
|
||||
if tryTimes > 1 {
|
||||
tryTimes--
|
||||
log.Info("ResumableBlockput retrying ...")
|
||||
goto lzRetry
|
||||
}
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func (p Uploader) mkfile(
|
||||
ctx Context, uphosts []string, ret interface{}, key string, hasKey bool, fsize int64, extra *RputExtra) (err error) {
|
||||
|
||||
url := uphosts[0] + "/mkfile/" + strconv.FormatInt(fsize, 10)
|
||||
|
||||
if extra.MimeType != "" {
|
||||
url += "/mimeType/" + encode(extra.MimeType)
|
||||
}
|
||||
if hasKey {
|
||||
url += "/key/" + encode(key)
|
||||
}
|
||||
for k, v := range extra.Params {
|
||||
url += fmt.Sprintf("/%s/%s", k, encode(v))
|
||||
}
|
||||
|
||||
buf := make([]byte, 0, 176*len(extra.Progresses))
|
||||
for _, prog := range extra.Progresses {
|
||||
buf = append(buf, prog.Ctx...)
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
if len(buf) > 0 {
|
||||
buf = buf[:len(buf)-1]
|
||||
}
|
||||
|
||||
return p.Conn.CallWith(
|
||||
ctx, ret, "POST", url, "application/octet-stream", bytes.NewReader(buf), len(buf))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func encode(raw string) string {
|
||||
return base64.URLEncoding.EncodeToString([]byte(raw))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
package kodocli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
const (
|
||||
DontCheckCrc = 0
|
||||
CalcAndCheckCrc = 1
|
||||
CheckCrc = 2
|
||||
)
|
||||
|
||||
// 上传的额外可选项
|
||||
//
|
||||
type PutExtra struct {
|
||||
// 可选,用户自定义参数,必须以 "x:" 开头。若不以x:开头,则忽略。
|
||||
Params map[string]string
|
||||
|
||||
// 可选,当为 "" 时候,服务端自动判断。
|
||||
MimeType string
|
||||
|
||||
Crc32 uint32
|
||||
|
||||
// CheckCrc == 0 (DontCheckCrc): 表示不进行 crc32 校验
|
||||
// CheckCrc == 1 (CalcAndCheckCrc): 对于 Put 等同于 CheckCrc = 2;对于 PutFile 会自动计算 crc32 值
|
||||
// CheckCrc == 2 (CheckCrc): 表示进行 crc32 校验,且 crc32 值就是上面的 Crc32 变量
|
||||
CheckCrc uint32
|
||||
|
||||
// 上传事件:进度通知。这个事件的回调函数应该尽可能快地结束。
|
||||
OnProgress func(fsize, uploaded int64)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 如果 uptoken 没有指定 ReturnBody,那么返回值是标准的 PutRet 结构
|
||||
//
|
||||
type PutRet struct {
|
||||
Hash string `json:"hash"`
|
||||
PersistentId string `json:"persistentId"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 上传一个文件。
|
||||
// 和 Put 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.Reader 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) PutFile(
|
||||
ctx Context, ret interface{}, uptoken, key, localFile string, extra *PutExtra) (err error) {
|
||||
|
||||
return p.putFile(ctx, ret, uptoken, key, true, localFile, extra)
|
||||
}
|
||||
|
||||
// 上传一个文件。文件的访问路径(key)自动生成。
|
||||
// 如果 uptoken 中设置了 SaveKey,那么按 SaveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。
|
||||
// 和 RputWithoutKey 不同的只是一个通过提供文件路径来访问文件内容,一个通过 io.Reader 来访问。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// localFile 是要上传的文件的本地路径。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) PutFileWithoutKey(
|
||||
ctx Context, ret interface{}, uptoken, localFile string, extra *PutExtra) (err error) {
|
||||
|
||||
return p.putFile(ctx, ret, uptoken, "", false, localFile, extra)
|
||||
}
|
||||
|
||||
func (p Uploader) putFile(
|
||||
ctx Context, ret interface{}, uptoken string,
|
||||
key string, hasKey bool, localFile string, extra *PutExtra) (err error) {
|
||||
|
||||
f, err := os.Open(localFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fsize := fi.Size()
|
||||
|
||||
if extra != nil && extra.CheckCrc == 1 {
|
||||
extra.Crc32, err = getFileCrc32(f)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return p.put(ctx, ret, uptoken, key, hasKey, f, fsize, extra, filepath.Base(localFile))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
// 上传一个文件。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// key 是要上传的文件访问路径。比如:"foo/bar.jpg"。注意我们建议 key 不要以 '/' 开头。另外,key 为空字符串是合法的。
|
||||
// data 是文件内容的访问接口(io.Reader)。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) Put(
|
||||
ctx Context, ret interface{}, uptoken, key string, data io.Reader, size int64, extra *PutExtra) error {
|
||||
|
||||
return p.put(ctx, ret, uptoken, key, true, data, size, extra, path.Base(key))
|
||||
}
|
||||
|
||||
// 上传一个文件。文件的访问路径(key)自动生成。
|
||||
// 如果 uptoken 中设置了 SaveKey,那么按 SaveKey 要求的规则生成 key,否则自动以文件的 hash 做 key。
|
||||
//
|
||||
// ctx 是请求的上下文。
|
||||
// ret 是上传成功后返回的数据。如果 uptoken 中没有设置 CallbackUrl 或 ReturnBody,那么返回的数据结构是 PutRet 结构。
|
||||
// uptoken 是由业务服务器颁发的上传凭证。
|
||||
// data 是文件内容的访问接口(io.Reader)。
|
||||
// fsize 是要上传的文件大小。
|
||||
// extra 是上传的一些可选项。详细见 PutExtra 结构的描述。
|
||||
//
|
||||
func (p Uploader) PutWithoutKey(
|
||||
ctx Context, ret interface{}, uptoken string, data io.Reader, size int64, extra *PutExtra) error {
|
||||
|
||||
return p.put(ctx, ret, uptoken, "", false, data, size, extra, "filename")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
var defaultPutExtra PutExtra
|
||||
|
||||
func (p Uploader) put(
|
||||
ctx Context, ret interface{}, uptoken string,
|
||||
key string, hasKey bool, data io.Reader, size int64, extra *PutExtra, fileName string) error {
|
||||
|
||||
uphosts, err := p.getUpHostFromToken(uptoken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
writer := multipart.NewWriter(&b)
|
||||
|
||||
if extra == nil {
|
||||
extra = &defaultPutExtra
|
||||
}
|
||||
|
||||
if extra.OnProgress != nil {
|
||||
data = &readerWithProgress{reader: data, fsize: size, onProgress: extra.OnProgress}
|
||||
}
|
||||
|
||||
err = writeMultipart(writer, uptoken, key, hasKey, extra, fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastLine := fmt.Sprintf("\r\n--%s--\r\n", writer.Boundary())
|
||||
r := strings.NewReader(lastLine)
|
||||
|
||||
bodyLen := int64(b.Len()) + size + int64(len(lastLine))
|
||||
mr := io.MultiReader(&b, data, r)
|
||||
|
||||
contentType := writer.FormDataContentType()
|
||||
err = p.Conn.CallWith64(ctx, ret, "POST", uphosts[0], contentType, mr, bodyLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if extra.OnProgress != nil {
|
||||
extra.OnProgress(size, size)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
type readerWithProgress struct {
|
||||
reader io.Reader
|
||||
uploaded int64
|
||||
fsize int64
|
||||
onProgress func(fsize, uploaded int64)
|
||||
}
|
||||
|
||||
func (p *readerWithProgress) Read(b []byte) (n int, err error) {
|
||||
|
||||
if p.uploaded > 0 {
|
||||
p.onProgress(p.fsize, p.uploaded)
|
||||
}
|
||||
|
||||
n, err = p.reader.Read(b)
|
||||
p.uploaded += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func writeMultipart(
|
||||
writer *multipart.Writer, uptoken, key string, hasKey bool, extra *PutExtra, fileName string) (err error) {
|
||||
|
||||
//token
|
||||
if err = writer.WriteField("token", uptoken); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//key
|
||||
if hasKey {
|
||||
if err = writer.WriteField("key", key); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//extra.Params
|
||||
if extra.Params != nil {
|
||||
for k, v := range extra.Params {
|
||||
if strings.HasPrefix(k, "x:") {
|
||||
err = writer.WriteField(k, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//extra.CheckCrc
|
||||
if extra.CheckCrc != 0 {
|
||||
err = writer.WriteField("crc32", strconv.FormatInt(int64(extra.Crc32), 10))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//file
|
||||
head := make(textproto.MIMEHeader)
|
||||
head.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, escapeQuotes(fileName)))
|
||||
if extra.MimeType != "" {
|
||||
head.Set("Content-Type", extra.MimeType)
|
||||
}
|
||||
|
||||
_, err = writer.CreatePart(head)
|
||||
return err
|
||||
}
|
||||
|
||||
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
|
||||
func escapeQuotes(s string) string {
|
||||
return quoteEscaper.Replace(s)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
func getFileCrc32(f *os.File) (uint32, error) {
|
||||
|
||||
h := crc32.NewIEEE()
|
||||
_, err := io.Copy(h, f)
|
||||
f.Seek(0, 0)
|
||||
|
||||
return h.Sum32(), err
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
Reference in New Issue
Block a user