mirror of
https://github.com/eiblog/eiblog.git
synced 2026-03-01 00:34:58 +08:00
add vendor
This commit is contained in:
+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))
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
Reference in New Issue
Block a user