update vendor

This commit is contained in:
deepzz0
2017-07-11 23:50:01 +08:00
parent e1ec5cd08a
commit c18d9c0bef
107 changed files with 8347 additions and 126 deletions

24
vendor/github.com/qiniu/api.v7/.gitignore generated vendored Normal file
View File

@@ -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

19
vendor/github.com/qiniu/api.v7/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,19 @@
language: go
go:
- 1.7
- 1.8
- master
env:
global:
- QINIU_KODO_TEST=1
- QINIU_ACCESS_KEY="QWYn5TFQsLLU1pL5MFEmX3s5DmHdUThav9WyOWOm"
- QINIU_SECRET_KEY="Bxckh6FA-Fbs9Yt3i3cbKVK22UPBmAOHJcL95pGz"
- QINIU_TEST_BUCKET="gosdk"
- QINIU_TEST_DOMAIN="gosdk.qiniudn.com"
install:
- export QINIU_SRC=$HOME/gopath/src
- mkdir -p $QINIU_SRC/github.com/qiniu
- export TRAVIS_BUILD_DIR=$QINIU_SRC/github.com/qiniu/api.v7
- cd $TRAVIS_BUILD_DIR
- go get github.com/qiniu/x

39
vendor/github.com/qiniu/api.v7/CHANGELOG.md generated vendored Normal file
View File

@@ -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
vendor/github.com/qiniu/api.v7/README.md generated vendored Normal file
View File

@@ -0,0 +1,25 @@
github.com/qiniu/api.v7 (Qiniu Go SDK v7.x)
===============
[![Build Status](https://travis-ci.org/qiniu/api.v7.svg?branch=develop)](https://travis-ci.org/qiniu/api.v7) [![GoDoc](https://godoc.org/github.com/qiniu/api.v7?status.svg)](https://godoc.org/github.com/qiniu/api.v7)
[![Qiniu Logo](http://open.qiniudn.com/logo.png)](http://qiniu.com/)
# 下载
```
go get -u github.com/qiniu/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 (七牛对象存储)
* [github.com/qiniu/api.v7/kodo](http://godoc.org/github.com/qiniu/api.v7/kodo)
* [github.com/qiniu/api.v7/kodocli](http://godoc.org/github.com/qiniu/api.v7/kodocli)
如果您是在业务服务器(服务器端)调用七牛云存储的服务,请使用 [github.com/qiniu/api.v7/kodo](http://godoc.org/github.com/qiniu/api.v7/kodo)。
如果您是在客户端比如Android/iOS 设备、Windows/Mac/Linux 桌面环境)调用七牛云存储的服务,请使用 [github.com/qiniu/api.v7/kodocli](http://godoc.org/github.com/qiniu/api.v7/kodocli)。注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,会影响您的数据安全。

99
vendor/github.com/qiniu/api.v7/api/api.go generated vendored Normal file
View File

@@ -0,0 +1,99 @@
package api
import (
. "context"
"sync"
"time"
"github.com/qiniu/x/rpc.v7"
)
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
vendor/github.com/qiniu/api.v7/auth/qbox/qbox_auth.go generated vendored Normal file
View File

@@ -0,0 +1,171 @@
package qbox
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"io"
"net/http"
. "github.com/qiniu/api.v7/conf"
"github.com/qiniu/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}
}
// ---------------------------------------------------------------------------------------

35
vendor/github.com/qiniu/api.v7/cdn/anti_leech.go generated vendored Normal file
View File

@@ -0,0 +1,35 @@
package cdn
import (
"crypto/md5"
"fmt"
"net/url"
"time"
)
// CreateTimestampAntileechURL 构建带时间戳防盗链的链接
// encryptKey 七牛防盗链key
func CreateTimestampAntileechURL(urlStr string, encryptKey string, durationInSeconds int64) (antileechURL string, err error) {
u, err := url.Parse(urlStr)
if err != nil {
return
}
expireTime := time.Now().Add(time.Second * time.Duration(durationInSeconds)).Unix()
toSignStr := fmt.Sprintf("%s%s%x", encryptKey, u.EscapedPath(), expireTime)
signedStr := fmt.Sprintf("%x", md5.Sum([]byte(toSignStr)))
q := url.Values{}
q.Add("sign", signedStr)
q.Add("t", fmt.Sprintf("%x", expireTime))
if u.RawQuery == "" {
antileechURL = u.String() + "?" + q.Encode()
} else {
antileechURL = u.String() + "&" + q.Encode()
}
return
}

37
vendor/github.com/qiniu/api.v7/cdn/anti_leech_test.go generated vendored Normal file
View File

@@ -0,0 +1,37 @@
package cdn
import (
"testing"
)
func TestCreateTimestampAntiLeech(t *testing.T) {
type args struct {
urlStr string
encryptKey string
durationInSeconds int64
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "antileech_1",
args: args{
urlStr: "http://www.abc.com/abc.jpg?stat",
encryptKey: "abc",
durationInSeconds: 20,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := CreateTimestampAntileechURL(tt.args.urlStr, tt.args.encryptKey, tt.args.durationInSeconds)
if (err != nil) != tt.wantErr {
t.Errorf("CreateTimestampAntiLeech() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}

260
vendor/github.com/qiniu/api.v7/cdn/api.go generated vendored Normal file
View File

@@ -0,0 +1,260 @@
package cdn
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/qiniu/api.v7/auth/qbox"
. "github.com/qiniu/api.v7/conf"
)
var (
FUSION_HOST = "http://fusion.qiniuapi.com"
)
/* TrafficReqBody
批量查询带宽/流量 请求内容
StartDate string 开始日期例如2016-07-01
EndDate string 结束日期例如2016-07-03
Granularity string 粒度取值5min hour day
Domains string 域名列表,以 ;分割
*/
type TrafficReqBody struct {
StartDate string `json:"startDate"`
EndDate string `json:"endDate"`
Granularity string `json:"granularity"`
Domains string `json:"domains"`
}
// TrafficResp
// 带宽/流量查询响应内容
type TrafficResp struct {
Code int `json:"code"`
Error string `json:"error"`
Time []string `json:"time,omitempty"`
Data map[string]TrafficRespData `json:"data,omitempty"`
}
// TrafficRespData
// 带宽/流量数据
type TrafficRespData struct {
DomainChina []int `json:"china"`
DomainOversea []int `json:"oversea"`
}
/*
// BandWidth
获取域名访问带宽数据
http://developer.qiniu.com/article/fusion/api/traffic-bandwidth.html
StartDate string 必须 开始日期例如2016-07-01
EndDate string 必须 结束日期例如2016-07-03
Granularity string 必须 粒度取值5min hour day
Domains []string 必须 域名列表
*/
func GetBandWidthData(startDate, endDate, granularity string, domainList []string) (bandwidthData TrafficResp, err error) {
domains := strings.Join(domainList, ";")
reqBody := TrafficReqBody{
StartDate: startDate,
EndDate: endDate,
Granularity: granularity,
Domains: domains,
}
resData, reqErr := postRequest("v2/tune/bandwidth", reqBody)
if reqErr != nil {
err = reqErr
return
}
umErr := json.Unmarshal(resData, &bandwidthData)
if umErr != nil {
err = umErr
return
}
return
}
/* Flux
获取域名访问流量数据
http://developer.qiniu.com/article/fusion/api/traffic-bandwidth.html
StartDate string 必须 开始日期例如2016-07-01
EndDate string 必须 结束日期例如2016-07-03
Granularity string 必须 粒度取值5min hour day
Domains []string 必须 域名列表
*/
func GetFluxData(startDate, endDate, granularity string, domainList []string) (fluxData TrafficResp, err error) {
domains := strings.Join(domainList, ";")
reqBody := TrafficReqBody{
StartDate: startDate,
EndDate: endDate,
Granularity: granularity,
Domains: domains,
}
resData, reqErr := postRequest("v2/tune/flux", reqBody)
if reqErr != nil {
err = reqErr
return
}
umErr := json.Unmarshal(resData, &fluxData)
if umErr != nil {
err = umErr
return
}
return
}
// RefreshReq
// 缓存刷新请求内容
type RefreshReq struct {
Urls []string `json:"urls"`
Dirs []string `json:"dirs"`
}
// RefreshResp
// 缓存刷新响应内容
type RefreshResp struct {
Code int `json:"code"`
Error string `json:"error"`
RequestID string `json:"requestId,omitempty"`
InvalidUrls []string `json:"invalidUrls,omitempty"`
InvalidDirs []string `json:"invalidDirs,omitempty"`
UrlQuotaDay int `json:"urlQuotaDay,omitempty"`
UrlSurplusDay int `json:"urlSurplusDay,omitempty"`
DirQuotaDay int `json:"dirQuotaDay,omitempty"`
DirSurplusDay int `json:"dirSurplusDay,omitempty"`
}
/* RefreshUrlsAndDirs
刷新链接列表每次最多不可以超过100条链接
http://developer.qiniu.com/article/fusion/api/refresh.html
urls 要刷新的单个url列表总数不超过100条单个url即一个具体的url例如http://bar.foo.com/index.html
dirs 要刷新的目录url列表总数不超过10条目录dir即表示一个目录级的url例如http://bar.foo.com/dir/也支持在尾部使用通配符例如http://bar.foo.com/dir/*
*/
func RefreshUrlsAndDirs(urls, dirs []string) (result RefreshResp, err error) {
reqBody := RefreshReq{
Urls: urls,
Dirs: dirs,
}
resData, reqErr := postRequest("v2/tune/refresh", reqBody)
if reqErr != nil {
err = reqErr
return
}
umErr := json.Unmarshal(resData, &result)
if umErr != nil {
err = reqErr
return
}
return
}
// RefreshUrls
// 刷新文件
func RefreshUrls(urls []string) (result RefreshResp, err error) {
return RefreshUrlsAndDirs(urls, nil)
}
// RefreshDirs
// 刷新目录
func RefreshDirs(dirs []string) (result RefreshResp, err error) {
return RefreshUrlsAndDirs(nil, dirs)
}
// PrefetchReq
// 文件预取请求内容
type PrefetchReq struct {
Urls []string `json:"urls"`
}
// PrefetchResp
// 文件预取响应内容
type PrefetchResp struct {
Code int `json:"code"`
Error string `json:"error"`
RequestID string `json:"requestId,omitempty"`
InvalidUrls []string `json:"invalidUrls,omitempty"`
QuotaDay int `json:"quotaDay,omitempty"`
SurplusDay int `json:"surplusDay,omitempty"`
}
// PrefetchUrls
// 预取文件链接每次最多不可以超过100条
// http://developer.qiniu.com/article/fusion/api/prefetch.html
func PrefetchUrls(urls []string) (result PrefetchResp, err error) {
reqBody := PrefetchReq{
Urls: urls,
}
resData, reqErr := postRequest("v2/tune/prefetch", reqBody)
if reqErr != nil {
err = reqErr
return
}
umErr := json.Unmarshal(resData, &result)
if umErr != nil {
err = umErr
return
}
return
}
// RequestWithBody
// 带body对api发出请求并且返回response body
func postRequest(path string, body interface{}) (resData []byte, err error) {
urlStr := fmt.Sprintf("%s/%s", FUSION_HOST, path)
reqData, _ := json.Marshal(body)
req, reqErr := http.NewRequest("POST", urlStr, bytes.NewReader(reqData))
if reqErr != nil {
err = reqErr
return
}
mac := qbox.NewMac(ACCESS_KEY, SECRET_KEY)
accessToken, signErr := mac.SignRequest(req, false)
if signErr != nil {
err = signErr
return
}
req.Header.Add("Authorization", "QBox "+accessToken)
req.Header.Add("Content-Type", "application/json")
resp, httpErr := http.DefaultClient.Do(req)
if httpErr != nil {
err = httpErr
return
}
defer resp.Body.Close()
resData, ioErr := ioutil.ReadAll(resp.Body)
if ioErr != nil {
err = ioErr
return
}
return
}

243
vendor/github.com/qiniu/api.v7/cdn/api_test.go generated vendored Normal file
View File

@@ -0,0 +1,243 @@
package cdn
import (
"math/rand"
"os"
"reflect"
"strconv"
"testing"
"time"
"github.com/qiniu/api.v7/kodo"
)
var (
ak = os.Getenv("QINIU_ACCESS_KEY")
sk = os.Getenv("QINIU_SECRET_KEY")
domain = os.Getenv("QINIU_TEST_DOMAIN")
testBucketName = os.Getenv("QINIU_TEST_BUCKET")
testDate = time.Now().AddDate(0, 0, -3).Format("2006-01-02")
bucket = newBucket()
client *kodo.Client
testKey = "fusionTest"
testURL string
)
func init() {
kodo.SetMac(ak, sk)
rand.Seed(time.Now().UnixNano())
testKey += strconv.Itoa(rand.Int())
bucket.PutFile(nil, nil, testKey, "doc.go", nil)
testURL = domain + "/" + testKey
}
func TestGetBandWidthData(t *testing.T) {
type args struct {
startDate string
endDate string
granularity string
domainList []string
}
tests := []struct {
name string
args args
wantTraffic TrafficResp
wantErr bool
}{
{
name: "BandWidthTest_1",
args: args{
testDate,
testDate,
"5min",
[]string{domain},
},
},
}
kodo.SetMac(ak, sk)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := GetBandWidthData(tt.args.startDate, tt.args.endDate, tt.args.granularity, tt.args.domainList)
if (err != nil) != tt.wantErr {
t.Errorf("GetBandWidthData() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestGetFluxData(t *testing.T) {
type args struct {
startDate string
endDate string
granularity string
domainList []string
}
tests := []struct {
name string
args args
wantTraffic TrafficResp
wantErr bool
}{
{
name: "BandWidthTest_1",
args: args{
testDate,
testDate,
"5min",
[]string{domain},
},
},
}
kodo.SetMac(ak, sk)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := GetFluxData(tt.args.startDate, tt.args.endDate, tt.args.granularity, tt.args.domainList)
if (err != nil) != tt.wantErr {
t.Errorf("GetFluxData() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestRefreshUrlsAndDirs(t *testing.T) {
kodo.SetMac(ak, sk)
type args struct {
urls []string
dirs []string
}
tests := []struct {
name string
args args
wantResult RefreshResp
wantErr bool
}{
{
name: "refresh_test_1",
args: args{
urls: []string{testURL},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RefreshUrlsAndDirs(tt.args.urls, tt.args.dirs)
if (err != nil) != tt.wantErr {
t.Errorf("RefreshUrlsAndDirs() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestRefreshUrls(t *testing.T) {
type args struct {
urls []string
}
tests := []struct {
name string
args args
wantResult RefreshResp
wantErr bool
}{
{
name: "refresh_test_1",
args: args{
urls: []string{testURL},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := RefreshUrls(tt.args.urls)
if (err != nil) != tt.wantErr {
t.Errorf("RefreshUrls() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestRefreshDirs(t *testing.T) {
type args struct {
dirs []string
}
tests := []struct {
name string
args args
wantResult RefreshResp
wantErr bool
}{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotResult, err := RefreshDirs(tt.args.dirs)
if (err != nil) != tt.wantErr {
t.Errorf("RefreshDirs() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotResult, tt.wantResult) {
t.Errorf("RefreshDirs() = %v, want %v", gotResult, tt.wantResult)
}
})
}
}
func TestPrefetchUrls(t *testing.T) {
type args struct {
urls []string
}
tests := []struct {
name string
args args
wantResult PrefetchResp
wantErr bool
}{
{
name: "refresh_test_1",
args: args{
urls: []string{testURL},
},
wantErr: false,
},
}
kodo.SetMac(ak, sk)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := PrefetchUrls(tt.args.urls)
if (err != nil) != tt.wantErr {
t.Errorf("PrefetchUrls() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func newBucket() (bucket kodo.Bucket) {
ak := os.Getenv("QINIU_ACCESS_KEY")
sk := os.Getenv("QINIU_SECRET_KEY")
if ak == "" || sk == "" {
panic("require ACCESS_KEY & SECRET_KEY")
}
kodo.SetMac(ak, sk)
testBucketName = os.Getenv("QINIU_TEST_BUCKET")
domain = os.Getenv("QINIU_TEST_DOMAIN")
if testBucketName == "" || domain == "" {
panic("require test env")
}
client = kodo.NewWithoutZone(nil)
return client.Bucket(testBucketName)
}

25
vendor/github.com/qiniu/api.v7/cdn/doc.go generated vendored Normal file
View File

@@ -0,0 +1,25 @@
/*
包 github.com/qiniu/api.v7/cdn 提供了七牛CDN的API功能
首先,我们要配置下 AccessKey/SecretKey,
import "github.com/qiniu/api.v7/kodo"
kodo.SetMac("ak", "sk")
设置了AccessKey/SecretKey 就可以使用cdn的各类功能
比如我们要生成一个带时间戳防盗链的链接:
q :=url.Values{}// url.Values 请求参数
link, err := cdn.CreateTimestampAntileechURL(""http://www.qiniu.com/abc/bcc/aa-s.mp4?x=2&y=3", "encryptedkey", 20)
if err != nil {
fmt.Println(err)
}
fmt.Println(link)
又或者我们要列出CDN日志及其下载地址
resp, err := cdn.GetCdnLogList("2016-12-26", "x-mas.com")
if err != nil {
fmt.Println(err)
}
fmt.Println(resp)
*/
package cdn

92
vendor/github.com/qiniu/api.v7/cdn/logs.go generated vendored Normal file
View File

@@ -0,0 +1,92 @@
package cdn
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/qiniu/api.v7/auth/qbox"
)
const (
LIST_LOG_API = "http://fusion.qiniuapi.com/v2/tune/log/list"
)
// ListLogRequest 日志下载请求内容
type ListLogRequest struct {
Day string `json:"day"`
Domains string `json:"domains"`
}
// ListLogResult 日志下载相应内容
type ListLogResult struct {
Code int `json:"code"`
Error string `json:"error"`
Data map[string][]LogDomainInfo `json:"data"`
}
// LogDomainInfo 日志下载信息
type LogDomainInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
ModifiedTime int64 `json:"mtime"`
URL string `json:"url"`
}
// GetCdnLogList 获取CDN域名访问日志的下载链接
// http://developer.qiniu.com/article/fusion/api/log.html
func GetCdnLogList(date, domains string) (domainLogs []LogDomainInfo, err error) {
//new log query request
logReq := ListLogRequest{
Day: date,
Domains: domains,
}
logReqBytes, _ := json.Marshal(&logReq)
req, reqErr := http.NewRequest("POST", LIST_LOG_API, bytes.NewReader(logReqBytes))
if reqErr != nil {
err = fmt.Errorf("New request error, %s", reqErr)
return
}
mac := qbox.NewMac("", "")
token, signErr := mac.SignRequest(req, false)
if signErr != nil {
err = signErr
return
}
req.Header.Add("Authorization", "QBox "+token)
req.Header.Add("Content-Type", "application/json")
resp, respErr := http.DefaultClient.Do(req)
if respErr != nil {
err = fmt.Errorf("Get response error, %s", respErr)
return
}
defer resp.Body.Close()
listLogResult := ListLogResult{}
decoder := json.NewDecoder(resp.Body)
if decodeErr := decoder.Decode(&listLogResult); decodeErr != nil {
err = fmt.Errorf("Parse response error, %s", decodeErr)
return
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("Get log list error, %d %s", listLogResult.Code, listLogResult.Error)
return
}
domainItems := strings.Split(domains, ";")
for _, domain := range domainItems {
for _, v := range listLogResult.Data[domain] {
domainLogs = append(domainLogs, v)
}
}
return
}

48
vendor/github.com/qiniu/api.v7/cdn/logs_test.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
package cdn
import (
"fmt"
"reflect"
"testing"
"github.com/qiniu/api.v7/kodo"
)
func init() {
kodo.SetMac(ak, sk)
}
func TestGetCdnLogList(t *testing.T) {
type args struct {
date string
domains string
}
tests := []struct {
name string
args args
wantDomainLogs []LogDomainInfo
wantErr bool
}{
{
name: "getCdnLogListTest",
args: args{
date: testDate,
domains: domain,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotDomainLogs, err := GetCdnLogList(tt.args.date, tt.args.domains)
if (err != nil) != tt.wantErr {
t.Errorf("GetCdnLogList() error = %v, wantErr %v", err, tt.wantErr)
return
}
fmt.Println(domain, gotDomainLogs)
if !reflect.DeepEqual(gotDomainLogs, tt.wantDomainLogs) {
t.Errorf("GetCdnLogList() = %v, want %v", gotDomainLogs, tt.wantDomainLogs)
}
})
}
}

38
vendor/github.com/qiniu/api.v7/conf/conf.go generated vendored Normal file
View File

@@ -0,0 +1,38 @@
package conf
import (
"fmt"
"runtime"
"syscall"
"github.com/qiniu/x/ctype.v7"
"github.com/qiniu/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
vendor/github.com/qiniu/api.v7/conf/conf_test.go generated vendored Normal file
View File

@@ -0,0 +1,35 @@
package conf
import (
"strings"
"testing"
"github.com/qiniu/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
vendor/github.com/qiniu/api.v7/doc.go generated vendored Normal file
View File

@@ -0,0 +1,23 @@
/*
包 github.com/qiniu/api.v7 是七牛 Go 语言 SDK v7.x 版本
七牛对象存储,我们取了一个好听的名字,叫 KODO Blob Storage。要使用它你主要和以下两个包打交道
import "github.com/qiniu/api.v7/kodo"
import "github.com/qiniu/api.v7/kodocli"
如果您是在业务服务器(服务器端)调用七牛云存储的服务,请使用 github.com/qiniu/api.v7/kodo。
如果您是在客户端比如Android/iOS 设备、Windows/Mac/Linux 桌面环境)调用七牛云存储的服务,请使用 github.com/qiniu/api.v7/kodocli。
注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,
会影响您的数据安全。
*/
package api
import (
_ "github.com/qiniu/api.v7/auth/qbox"
_ "github.com/qiniu/api.v7/cdn"
_ "github.com/qiniu/api.v7/conf"
_ "github.com/qiniu/api.v7/kodo"
_ "github.com/qiniu/api.v7/kodocli"
)

435
vendor/github.com/qiniu/api.v7/kodo/bucket.go generated vendored Normal file
View File

@@ -0,0 +1,435 @@
package kodo
import (
. "context"
"encoding/base64"
"fmt"
"io"
"net/url"
"strconv"
"github.com/qiniu/api.v7/api"
"github.com/qiniu/x/log.v7"
)
// ----------------------------------------------------------
// Batch 批量操作。
//
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
}
// Buckets 获取所有地区的所有空间(bucket)
//
// shared 是否获取所有授权获得空间true为包含授权空间
//
func (p *Client) Buckets(ctx Context, shared bool) (buckets []string, err error) {
if shared {
err = p.Call(ctx, &buckets, "POST", p.RSHost+"/buckets?shared=trye")
} else {
err = p.Call(ctx, &buckets, "POST", p.RSHost+"/buckets")
}
return
}
// Bucket 取七牛空间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
}
// BucketWithSafe 确认空间存在并获取七牛空间bucket的对象实例。
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
}
// Entry 资源元信息
type Entry struct {
Hash string `json:"hash"`
Fsize int64 `json:"fsize"`
PutTime int64 `json:"putTime"`
MimeType string `json:"mimeType"`
Type int `json:"type"`
EndUser string `json:"endUser"`
}
// Stat 取文件属性。
//
// 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
}
// Delete 删除一个文件。
//
// 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))
}
// Move 移动一个文件。
//
// 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))
}
// MoveEx 跨空间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))
}
// Copy 复制一个文件。
//
// 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))
}
// ChangeMime 修改文件的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))
}
// ChangeType 修改文件的存储类型。
//
// ctx 是请求的上下文。
// key 是要修改的文件的访问路径。
// fileType 是要设置的新存储类型。0 表示标准存储1 表示低频存储。
//
func (p Bucket) ChangeType(ctx Context, key string, fileType int) (err error) {
return p.Conn.Call(ctx, nil, "POST", p.Conn.RSHost+URIChangeType(p.Name, key, fileType))
}
// Fetch 从网上抓取一个资源并存储到七牛空间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))
}
// DeleteAfterDays 更新文件生命周期
//
// ctx 是请求的上下文。
// key 是要更新的文件的访问路径。
// deleteAfterDays 设置为0表示取消 lifecycle
//
func (p Bucket) DeleteAfterDays(ctx Context, key string, days int) (err error) {
return p.Conn.Call(ctx, nil, "POST", p.Conn.RSHost+URIDeleteAfterDays(p.Name, key, days))
}
// Image 设置镜像源
//
// srcSiteURL 镜像源的访问域名。必须设置为形如 `http://source.com/` 或 `http://114.114.114.114/` 的字符串
// host 回源时使用的 Host 头部值
//
// 镜像源地址支持两种格式:
// 格式 1`http(s)://绑定域名/源站资源相对路径`
// 格式 2`http(s)://绑定 IP/源站资源相对路径`
//
func (p Bucket) Image(ctx Context, srcSiteURL, host string) (err error) {
return p.Conn.Call(ctx, nil, "POST", "http://pu.qbox.me:10200"+URIImage(p.Name, srcSiteURL, host))
}
// UnImage 取消镜像源
//
func (p Bucket) UnImage(ctx Context) (err error) {
return p.Conn.Call(ctx, nil, "POST", "http://pu.qbox.me:10200"+URIUnImage(p.Name))
}
// Prefetch 镜像资源更新
//
// key 被抓取资源名称
//
func (p Bucket) Prefetch(ctx Context, key string) (err error) {
return p.Conn.Call(ctx, nil, "POST", p.Conn.IoHost+URIPrefetch(p.Name, key))
}
// PfopResult pfop返回信息
type PfopResult struct {
PersistentID string `json:"persistentId,omitempty"`
}
// FopRet 持久化云处理结果
type FopRet struct {
ID string `json:"id"`
Code int `json:"code"`
Desc string `json:"desc"`
InputBucket string `json:"inputBucket,omitempty"`
InputKey string `json:"inputKey,omitempty"`
Pipeline string `json:"pipeline,omitempty"`
Reqid string `json:"reqid,omitempty"`
Items []FopResult
}
// FopResult 云处理操作列表,包含每个云处理操作的状态信息
type FopResult struct {
Cmd string `json:"cmd"`
Code int `json:"code"`
Desc string `json:"desc"`
Error string `json:"error,omitempty"`
Hash string `json:"hash,omitempty"`
Key string `json:"key,omitempty"`
Keys []string `json:"keys,omitempty"`
}
// Pfop 持久化数据处理
//
// bucket 资源空间
// key 源资源名
// fops 云处理操作列表,用`;``分隔,如:`avthumb/flv;saveas/cWJ1Y2tldDpxa2V5`是将上传的视频文件转码成flv格式后存储为 qbucket:qkey ,其中 cWJ1Y2tldDpxa2V5 是 qbucket:qkey 的URL安全的Base64编码结果。
// notifyURL 处理结果通知接收 URL七牛将会向你设置的 URL 发起 Content-Type: application/json 的 POST 请求。
// pipeline 为空则表示使用公用队列,处理速度比较慢。建议指定私有队列,转码的时候使用独立的计算资源。
// force 强制执行数据处理。当服务端发现 fops 指定的数据处理结果已经存在,那就认为已经处理成功,避免重复处理浪费资源。本字段设为 `true`,则可强制执行数据处理并覆盖原结果。
//
func (p *Client) Pfop(ctx Context, bucket, key, fops, notifyURL, pipeline string, force bool) (persistentID string, err error) {
pfopParams := map[string][]string{
"bucket": []string{bucket},
"key": []string{key},
"fops": []string{fops},
}
if notifyURL != "" {
pfopParams["notifyURL"] = []string{notifyURL}
}
if pipeline != "" {
pfopParams["pipeline"] = []string{pipeline}
}
if force {
pfopParams["force"] = []string{"1"}
}
var ret PfopResult
err = p.CallWithForm(ctx, &ret, "POST", "http://api.qiniu.com/pfop/", pfopParams)
if err != nil {
return
}
persistentID = ret.PersistentID
return
}
// Prefop 持久化处理状态查询
func (p *Client) Prefop(ctx Context, persistentID string) (ret FopRet, err error) {
err = p.Call(ctx, &ret, "GET", "http://api.qiniu.com/status/get/prefop?id="+persistentID)
return
}
// ----------------------------------------------------------
// ListItem List借口返回结果
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"`
}
// List 首次请求,请将 marker 设置为 ""。
// 无论 err 值如何,均应该先看 entries 是否有内容。
// 如果后续没有更多数据err 返回 EOFmarkerOut 返回 ""(但不通过该特征来判断是否结束)。
//
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"`
}
// BatchStat 批量取文件属性
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"`
}
// BatchDelete 批量删除
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
}
// BatchMove 批量移动文件
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
}
// BatchCopy 批量复制文件
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)
}
func URIChangeType(bucket, key string, fileType int) string {
return "/chtype/" + encodeURI(bucket+":"+key) + "/type/" + strconv.Itoa(fileType)
}
func URIDeleteAfterDays(bucket, key string, days int) string {
return fmt.Sprintf("/deleteAfterDays/%s/%d", encodeURI(bucket+":"+key), days)
}
func URIImage(bucket, srcSiteURL, host string) string {
return fmt.Sprintf("/image/%s/from/%s/host/%s", bucket, encodeURI(srcSiteURL), encodeURI(host))
}
func URIUnImage(bucket string) string {
return fmt.Sprintf("/unimage/%s", bucket)
}
func URIPrefetch(bucket, key string) string {
return fmt.Sprintf("/prefetch/%s", encodeURI(bucket+":"+key))
}
// ----------------------------------------------------------

167
vendor/github.com/qiniu/api.v7/kodo/bucket_test.go generated vendored Normal file
View File

@@ -0,0 +1,167 @@
package kodo
import (
"context"
"math/rand"
"strconv"
"testing"
"time"
)
var (
batchTestKey = "abatch"
batchTestNewKey1 = "abatch/newkey1"
batchTestNewKey2 = "abatch/newkey2"
)
func init() {
if skipTest() {
return
}
rand.Seed(time.Now().UnixNano())
batchTestKey += strconv.Itoa(rand.Int())
batchTestNewKey1 += strconv.Itoa(rand.Int())
batchTestNewKey2 += strconv.Itoa(rand.Int())
// 删除 可能存在的 key
bucket.BatchDelete(nil, batchTestKey, batchTestNewKey1, batchTestNewKey2)
}
func TestAll(t *testing.T) {
if skipTest() {
return
}
//上传一个文件用用于测试
err := upFile("bucket_test.go", batchTestKey)
if err != nil {
t.Fatal(err)
}
defer bucket.Delete(nil, batchTestKey)
testBatchStat(t)
testBatchCopy(t)
testBatchMove(t)
testBatchDelete(t)
testBatch(t)
testClient_MakeUptokenBucket(t)
testDeleteAfterDays(t)
}
func testBatchStat(t *testing.T) {
rets, err := bucket.BatchStat(nil, batchTestKey, batchTestKey, batchTestKey)
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, batchTestKey)
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, batchTestKey)
if err != nil {
t.Fatal("BathMove get stat failed:", err)
}
_, err = bucket.BatchMove(nil, KeyPair{batchTestKey, batchTestNewKey1}, KeyPair{batchTestNewKey1, batchTestNewKey2})
if err != nil {
t.Fatal("bucket.BatchMove failed:", err)
}
defer bucket.Move(nil, batchTestNewKey2, batchTestKey)
stat1, err := bucket.Stat(nil, batchTestNewKey2)
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{batchTestKey, batchTestNewKey1}, KeyPair{batchTestKey, batchTestNewKey2})
if err != nil {
t.Fatal(err)
}
defer bucket.Delete(nil, batchTestNewKey1)
defer bucket.Delete(nil, batchTestNewKey2)
stat0, _ := bucket.Stat(nil, batchTestKey)
stat1, _ := bucket.Stat(nil, batchTestNewKey1)
stat2, _ := bucket.Stat(nil, batchTestNewKey2)
if stat0.Hash != stat1.Hash || stat0.Hash != stat2.Hash {
t.Fatal("BatchCopy failed : Copy err")
}
}
func testBatchDelete(t *testing.T) {
bucket.Copy(nil, batchTestKey, batchTestNewKey1)
bucket.Copy(nil, batchTestKey, batchTestNewKey2)
_, err := bucket.BatchDelete(nil, batchTestNewKey1, batchTestNewKey2)
if err != nil {
t.Fatal(err)
}
_, err1 := bucket.Stat(nil, batchTestNewKey1)
_, err2 := bucket.Stat(nil, batchTestNewKey2)
//这里 err1 != nil否则文件没被成功删除
if err1 == nil || err2 == nil {
t.Fatal("BatchDelete failed : File do not delete")
}
}
func testBatch(t *testing.T) {
ops := []string{
URICopy(bucketName, batchTestKey, bucketName, batchTestNewKey1),
URIDelete(bucketName, batchTestKey),
URIMove(bucketName, batchTestNewKey1, bucketName, batchTestKey),
}
var rets []BatchItemRet
err := client.Batch(nil, &rets, ops)
if err != nil {
t.Fatal(err)
}
if len(rets) != 3 {
t.Fatal("len(rets) != 3")
}
}
func testDeleteAfterDays(t *testing.T) {
ctx := context.Background()
err := bucket.DeleteAfterDays(ctx, batchTestNewKey1, 5)
if err == nil {
t.Fatal("Expect an error")
}
bucket.Copy(ctx, batchTestKey, batchTestNewKey1)
err = bucket.DeleteAfterDays(ctx, batchTestNewKey1, 5)
if err != nil {
t.Fatal(err)
}
}

52
vendor/github.com/qiniu/api.v7/kodo/doc.go generated vendored Normal file
View File

@@ -0,0 +1,52 @@
/*
包 github.com/qiniu/api.v7/kodo 提供了在您的业务服务器(服务端)调用七牛云存储服务的能力
首先,我们要配置下 AccessKey/SecretKey这可以在七牛 Portal 中查到:
kodo.SetMac("your-access-key", "your-secret-key")
然后我们创建一个 Client 对象:
zone := kodo.ZoneZ0 // 您空间(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

154
vendor/github.com/qiniu/api.v7/kodo/main.go generated vendored Normal file
View File

@@ -0,0 +1,154 @@
package kodo
import (
"net/http"
"github.com/qiniu/api.v7/api"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/api.v7/conf"
"github.com/qiniu/x/rpc.v7"
)
// ----------------------------------------------------------
type zoneConfig struct {
IoHost string
UpHosts []string
}
const (
// ZoneZ0 华东机房
ZoneZ0 = iota
// ZoneZ1 华北机房
ZoneZ1
// ZoneZ2 华南机房
ZoneZ2
// ZoneNa0 北美机房
ZoneNa0
)
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
vendor/github.com/qiniu/api.v7/kodo/main_test.go generated vendored Normal file
View File

@@ -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 github.com/qiniu/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)
}
}

119
vendor/github.com/qiniu/api.v7/kodo/token.go generated vendored Normal file
View File

@@ -0,0 +1,119 @@
package kodo
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/qiniu/api.v7/api"
"github.com/qiniu/api.v7/auth/qbox"
"github.com/qiniu/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"`
FileType int `json:"fileType,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
vendor/github.com/qiniu/api.v7/kodo/token_test.go generated vendored Normal file
View File

@@ -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")
}
}

179
vendor/github.com/qiniu/api.v7/kodo/upload.go generated vendored Normal file
View File

@@ -0,0 +1,179 @@
package kodo
import (
. "context"
"io"
"net/http"
"github.com/qiniu/api.v7/kodocli"
"github.com/qiniu/x/rpc.v7"
)
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}
}
// ----------------------------------------------------------
// Put 上传一个文件。
//
// 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))
}
// PutWithoutKey 上传一个文件。自动以文件的 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))
}
// PutFile 上传一个文件。
// 和 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))
}
// PutFileWithoutKey 上传一个文件。自动以文件的 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))
}
// ----------------------------------------------------------
// Rput 上传一个文件,支持断点续传和分块上传。
//
// 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))
}
// RputWithoutKey 上传一个文件,支持断点续传和分块上传。自动以文件的 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))
}
// RputFile 上传一个文件,支持断点续传和分块上传。
// 和 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))
}
// RputFileWithoutKey 上传一个文件,支持断点续传和分块上传。自动以文件的 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
vendor/github.com/qiniu/api.v7/kodocli/doc.go generated vendored Normal file
View File

@@ -0,0 +1,67 @@
/*
包 github.com/qiniu/api.v7/kodocli 提供了在客户端调用七牛云存储部分服务的能力
客户端严谨说是非可信环境主要是指在用户端执行的环境比如Android/iOS 设备、Windows/Mac/Linux 桌面环境、也包括浏览器(如果浏览器能够执行 Go 语言代码的话)。
注意,在这种场合下您不应该在任何地方配置 AccessKey/SecretKey。泄露 AccessKey/SecretKey 如同泄露您的用户名/密码一样十分危险,会影响您的数据安全。
第一个问题是如何上传文件。因为是在非可信环境,所以我们首先是要授予它有上传文件的能力。答案是给它颁发上传凭证。假设服务端也是 Go 语言,那么:
import "github.com/qiniu/api.v7/kodo"
kodo.SetMac("your-access-key", "your-secret-key") // 配置 AccessKey/SecretKey
zone := kodo.ZoneZ0
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
vendor/github.com/qiniu/api.v7/kodocli/main.go generated vendored Normal file
View File

@@ -0,0 +1,114 @@
package kodocli
import (
"net/http"
"github.com/qiniu/api.v7/api"
"github.com/qiniu/api.v7/conf"
"github.com/qiniu/x/rpc.v7"
"github.com/qiniu/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)
}
// ----------------------------------------------------------

328
vendor/github.com/qiniu/api.v7/kodocli/resumable.go generated vendored Normal file
View File

@@ -0,0 +1,328 @@
package kodocli
import (
. "context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"os"
"strings"
"sync"
"github.com/qiniu/x/xlog.v7"
)
// ----------------------------------------------------------
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 = 1 * 1024 * 1024 // 1M
defaultTryTimes = 3
)
type Settings struct {
TaskQsize int // 可选。任务队列大小。为 0 表示取 Workers * 4。
Workers int // 并行 Goroutine 数目。
ChunkSize int // 默认的Chunk大小不设定则为1M
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)
}
// ----------------------------------------------------------

View File

@@ -0,0 +1,172 @@
package kodocli
import (
. "context"
"encoding/base64"
"fmt"
"hash/crc32"
"io"
"net/http"
"strconv"
"github.com/qiniu/x/bytes.v7"
"github.com/qiniu/x/rpc.v7"
"github.com/qiniu/x/xlog.v7"
)
// ----------------------------------------------------------
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))
}
// ----------------------------------------------------------

282
vendor/github.com/qiniu/api.v7/kodocli/upload.go generated vendored Normal file
View File

@@ -0,0 +1,282 @@
package kodocli
import (
"bytes"
. "context"
"fmt"
"hash/crc32"
"io"
"mime/multipart"
"net/textproto"
"os"
"path"
"path/filepath"
"strconv"
"strings"
)
// ----------------------------------------------------------
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(-1)
if size >= 0 {
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
}
// ----------------------------------------------------------