add multidownload
This commit is contained in:
57
example/object/download.go
Normal file
57
example/object/download.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"github.com/tencentyun/cos-go-sdk-v5"
|
||||||
|
"github.com/tencentyun/cos-go-sdk-v5/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
func log_status(err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cos.IsNotFoundError(err) {
|
||||||
|
// WARN
|
||||||
|
fmt.Println("WARN: Resource is not existed")
|
||||||
|
} else if e, ok := cos.IsCOSError(err); ok {
|
||||||
|
fmt.Printf("ERROR: Code: %v\n", e.Code)
|
||||||
|
fmt.Printf("ERROR: Message: %v\n", e.Message)
|
||||||
|
fmt.Printf("ERROR: Resource: %v\n", e.Resource)
|
||||||
|
fmt.Printf("ERROR: RequestId: %v\n", e.RequestID)
|
||||||
|
// ERROR
|
||||||
|
} else {
|
||||||
|
fmt.Printf("ERROR: %v\n", err)
|
||||||
|
// ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
u, _ := url.Parse("https://test-1259654469.cos.ap-guangzhou.myqcloud.com")
|
||||||
|
b := &cos.BaseURL{BucketURL: u}
|
||||||
|
c := cos.NewClient(b, &http.Client{
|
||||||
|
Transport: &cos.AuthorizationTransport{
|
||||||
|
SecretID: os.Getenv("COS_SECRETID"),
|
||||||
|
SecretKey: os.Getenv("COS_SECRETKEY"),
|
||||||
|
Transport: &debug.DebugRequestTransport{
|
||||||
|
RequestHeader: false,
|
||||||
|
RequestBody: false,
|
||||||
|
ResponseHeader: false,
|
||||||
|
ResponseBody: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
opt := &cos.MultiDownloadOptions{
|
||||||
|
ThreadPoolSize: 5,
|
||||||
|
}
|
||||||
|
resp, err := c.Object.Download(
|
||||||
|
context.Background(), "test", "./test1G", opt,
|
||||||
|
)
|
||||||
|
log_status(err)
|
||||||
|
fmt.Printf("done, %v\n", resp.Header)
|
||||||
|
}
|
||||||
23
helper.go
23
helper.go
@@ -237,3 +237,26 @@ func cloneObjectUploadPartOptions(opt *ObjectUploadPartOptions) *ObjectUploadPar
|
|||||||
}
|
}
|
||||||
return &res
|
return &res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RangeOptions struct {
|
||||||
|
HasStart bool
|
||||||
|
HasEnd bool
|
||||||
|
Start int64
|
||||||
|
End int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatRangeOptions(opt *RangeOptions) string {
|
||||||
|
if opt == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if opt.HasStart && opt.HasEnd {
|
||||||
|
return fmt.Sprintf("bytes=%v-%v", opt.Start, opt.End)
|
||||||
|
}
|
||||||
|
if opt.HasStart {
|
||||||
|
return fmt.Sprintf("bytes=%v-", opt.Start)
|
||||||
|
}
|
||||||
|
if opt.HasEnd {
|
||||||
|
return fmt.Sprintf("bytes=-%v", opt.End)
|
||||||
|
}
|
||||||
|
return "bytes=-"
|
||||||
|
}
|
||||||
|
|||||||
193
object.go
193
object.go
@@ -553,6 +553,12 @@ type MultiUploadOptions struct {
|
|||||||
EnableVerification bool
|
EnableVerification bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MultiDownloadOptions struct {
|
||||||
|
Opt *ObjectGetOptions
|
||||||
|
PartSize int64
|
||||||
|
ThreadPoolSize int
|
||||||
|
}
|
||||||
|
|
||||||
type Chunk struct {
|
type Chunk struct {
|
||||||
Number int
|
Number int
|
||||||
OffSet int64
|
OffSet int64
|
||||||
@@ -570,6 +576,7 @@ type Jobs struct {
|
|||||||
Chunk Chunk
|
Chunk Chunk
|
||||||
Data io.Reader
|
Data io.Reader
|
||||||
Opt *ObjectUploadPartOptions
|
Opt *ObjectUploadPartOptions
|
||||||
|
DownOpt *ObjectGetOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type Results struct {
|
type Results struct {
|
||||||
@@ -632,6 +639,48 @@ func worker(s *ObjectService, jobs <-chan *Jobs, results chan<- *Results) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadWorker(s *ObjectService, jobs <-chan *Jobs, results chan<- *Results) {
|
||||||
|
for j := range jobs {
|
||||||
|
opt := &RangeOptions{
|
||||||
|
HasStart: true,
|
||||||
|
HasEnd: true,
|
||||||
|
Start: j.Chunk.OffSet,
|
||||||
|
End: j.Chunk.OffSet + j.Chunk.Size - 1,
|
||||||
|
}
|
||||||
|
j.DownOpt.Range = FormatRangeOptions(opt)
|
||||||
|
rt := j.RetryTimes
|
||||||
|
for {
|
||||||
|
var res Results
|
||||||
|
res.PartNumber = j.Chunk.Number
|
||||||
|
resp, err := s.Get(context.Background(), j.Name, j.DownOpt)
|
||||||
|
res.err = err
|
||||||
|
res.Resp = resp
|
||||||
|
if err != nil {
|
||||||
|
rt--
|
||||||
|
if rt == 0 {
|
||||||
|
results <- &res
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
fd, err := os.OpenFile(j.FilePath, os.O_WRONLY, 0660)
|
||||||
|
if err != nil {
|
||||||
|
res.err = err
|
||||||
|
results <- &res
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fd.Seek(j.Chunk.OffSet, os.SEEK_SET)
|
||||||
|
n, err := io.Copy(fd, LimitReadCloser(resp.Body, j.Chunk.Size))
|
||||||
|
if n != j.Chunk.Size || err != nil {
|
||||||
|
res.err = fmt.Errorf("io.Copy Failed, read:%v, size:%v, err:%v", n, j.Chunk.Size, err)
|
||||||
|
}
|
||||||
|
results <- &res
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func DividePart(fileSize int64, last int) (int64, int64) {
|
func DividePart(fileSize int64, last int) (int64, int64) {
|
||||||
partSize := int64(last * 1024 * 1024)
|
partSize := int64(last * 1024 * 1024)
|
||||||
partNum := fileSize / partSize
|
partNum := fileSize / partSize
|
||||||
@@ -953,6 +1002,150 @@ func (s *ObjectService) Upload(ctx context.Context, name string, filepath string
|
|||||||
return v, resp, err
|
return v, resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SplitSizeIntoChunks(totalBytes int64, partSize int64) ([]Chunk, int, error) {
|
||||||
|
var partNum int64
|
||||||
|
if partSize > 0 {
|
||||||
|
partSize = partSize * 1024 * 1024
|
||||||
|
partNum = totalBytes / partSize
|
||||||
|
if partNum >= 10000 {
|
||||||
|
return nil, 0, errors.New("Too manry parts, out of 10000")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
partNum, partSize = DividePart(totalBytes, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunks []Chunk
|
||||||
|
var chunk = Chunk{}
|
||||||
|
for i := int64(0); i < partNum; i++ {
|
||||||
|
chunk.Number = int(i + 1)
|
||||||
|
chunk.OffSet = i * partSize
|
||||||
|
chunk.Size = partSize
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalBytes%partSize > 0 {
|
||||||
|
chunk.Number = len(chunks) + 1
|
||||||
|
chunk.OffSet = int64(len(chunks)) * partSize
|
||||||
|
chunk.Size = totalBytes % partSize
|
||||||
|
chunks = append(chunks, chunk)
|
||||||
|
partNum++
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks, int(partNum), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ObjectService) Download(ctx context.Context, name string, filepath string, opt *MultiDownloadOptions) (*Response, error) {
|
||||||
|
// 参数校验
|
||||||
|
if opt == nil {
|
||||||
|
opt = &MultiDownloadOptions{}
|
||||||
|
}
|
||||||
|
if opt.Opt != nil && opt.Opt.Range != "" {
|
||||||
|
return nil, fmt.Errorf("does not supported Range Get")
|
||||||
|
}
|
||||||
|
// 获取文件长度和CRC
|
||||||
|
var coscrc string
|
||||||
|
resp, err := s.Head(ctx, name, nil)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
coscrc = resp.Header.Get("x-cos-hash-crc64ecma")
|
||||||
|
strTotalBytes := resp.Header.Get("Content-Length")
|
||||||
|
totalBytes, err := strconv.ParseInt(strTotalBytes, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切分
|
||||||
|
chunks, partNum, err := SplitSizeIntoChunks(totalBytes, opt.PartSize)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
// 直接下载到文件
|
||||||
|
if partNum == 0 || partNum == 1 {
|
||||||
|
rsp, err := s.GetToFile(ctx, name, filepath, opt.Opt)
|
||||||
|
if err != nil {
|
||||||
|
return rsp, err
|
||||||
|
}
|
||||||
|
if coscrc != "" && s.client.Conf.EnableCRC {
|
||||||
|
icoscrc, _ := strconv.ParseUint(coscrc, 10, 64)
|
||||||
|
fd, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return rsp, err
|
||||||
|
}
|
||||||
|
localcrc, err := calCRC64(fd)
|
||||||
|
if err != nil {
|
||||||
|
return rsp, err
|
||||||
|
}
|
||||||
|
if localcrc != icoscrc {
|
||||||
|
return rsp, fmt.Errorf("verification failed, want:%v, return:%v", icoscrc, localcrc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rsp, err
|
||||||
|
}
|
||||||
|
nfile, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
nfile.Close()
|
||||||
|
var poolSize int
|
||||||
|
if opt.ThreadPoolSize > 0 {
|
||||||
|
poolSize = opt.ThreadPoolSize
|
||||||
|
} else {
|
||||||
|
poolSize = 1
|
||||||
|
}
|
||||||
|
chjobs := make(chan *Jobs, 100)
|
||||||
|
chresults := make(chan *Results, 10000)
|
||||||
|
for w := 1; w <= poolSize; w++ {
|
||||||
|
go downloadWorker(s, chjobs, chresults)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for _, chunk := range chunks {
|
||||||
|
var downOpt ObjectGetOptions
|
||||||
|
if opt.Opt != nil {
|
||||||
|
downOpt = *opt.Opt
|
||||||
|
}
|
||||||
|
job := &Jobs{
|
||||||
|
Name: name,
|
||||||
|
RetryTimes: 3,
|
||||||
|
FilePath: filepath,
|
||||||
|
Chunk: chunk,
|
||||||
|
DownOpt: &downOpt,
|
||||||
|
}
|
||||||
|
chjobs <- job
|
||||||
|
}
|
||||||
|
close(chjobs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = nil
|
||||||
|
for i := 0; i < partNum; i++ {
|
||||||
|
res := <-chresults
|
||||||
|
if res.Resp == nil || res.err != nil {
|
||||||
|
err = fmt.Errorf("part %d get resp Content. error: %s", res.PartNumber, res.err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(chresults)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if coscrc != "" && s.client.Conf.EnableCRC {
|
||||||
|
icoscrc, _ := strconv.ParseUint(coscrc, 10, 64)
|
||||||
|
fd, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
localcrc, err := calCRC64(fd)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if localcrc != icoscrc {
|
||||||
|
return resp, fmt.Errorf("verification failed, want:%v, return:%v", icoscrc, localcrc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
type ObjectPutTaggingOptions struct {
|
type ObjectPutTaggingOptions struct {
|
||||||
XMLName xml.Name `xml:"Tagging"`
|
XMLName xml.Name `xml:"Tagging"`
|
||||||
TagSet []ObjectTaggingTag `xml:"TagSet>Tag,omitempty"`
|
TagSet []ObjectTaggingTag `xml:"TagSet>Tag,omitempty"`
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc64"
|
"hash/crc64"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -514,3 +516,80 @@ func TestObjectService_Upload2(t *testing.T) {
|
|||||||
retry++
|
retry++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestObjectService_Download(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
filePath := "rsp.file" + time.Now().Format(time.RFC3339)
|
||||||
|
newfile, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tmp file failed")
|
||||||
|
}
|
||||||
|
defer os.Remove(filePath)
|
||||||
|
// 源文件内容
|
||||||
|
totalBytes := int64(1024 * 1024 * 10)
|
||||||
|
b := make([]byte, totalBytes)
|
||||||
|
_, err = rand.Read(b)
|
||||||
|
newfile.Write(b)
|
||||||
|
newfile.Close()
|
||||||
|
tb := crc64.MakeTable(crc64.ECMA)
|
||||||
|
localcrc := crc64.Update(0, tb, b)
|
||||||
|
|
||||||
|
retryMap := make(map[int64]int)
|
||||||
|
mux.HandleFunc("/test.go.download", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodHead {
|
||||||
|
w.Header().Add("Content-Length", strconv.FormatInt(totalBytes, 10))
|
||||||
|
w.Header().Add("x-cos-hash-crc64ecma", strconv.FormatUint(localcrc, 10))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strRange := r.Header.Get("Range")
|
||||||
|
slice1 := strings.Split(strRange, "=")
|
||||||
|
slice2 := strings.Split(slice1[1], "-")
|
||||||
|
start, _ := strconv.ParseInt(slice2[0], 10, 64)
|
||||||
|
end, _ := strconv.ParseInt(slice2[1], 10, 64)
|
||||||
|
if retryMap[start] == 0 {
|
||||||
|
retryMap[start]++
|
||||||
|
w.WriteHeader(http.StatusGatewayTimeout)
|
||||||
|
} else if retryMap[start] == 1 {
|
||||||
|
retryMap[start]++
|
||||||
|
w.WriteHeader(http.StatusGatewayTimeout)
|
||||||
|
return
|
||||||
|
fd, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open file failed: %v", err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
w.Header().Add("x-cos-hash-crc64ecma", strconv.FormatUint(localcrc, 10))
|
||||||
|
fd.Seek(start, os.SEEK_SET)
|
||||||
|
n, err := io.Copy(w, LimitReadCloser(fd, (end-start)/2))
|
||||||
|
if err != nil || int64(n) != (end-start)/2 {
|
||||||
|
t.Fatalf("write file failed:%v, n:%v", err, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
fd, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open file failed: %v", err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
w.Header().Add("x-cos-hash-crc64ecma", strconv.FormatUint(localcrc, 10))
|
||||||
|
fd.Seek(start, os.SEEK_SET)
|
||||||
|
n, err := io.Copy(w, LimitReadCloser(fd, end-start+1))
|
||||||
|
if err != nil || int64(n) != end-start+1 {
|
||||||
|
t.Fatalf("write file failed:%v, n:%v", err, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
opt := &MultiDownloadOptions{
|
||||||
|
ThreadPoolSize: 3,
|
||||||
|
PartSize: 1,
|
||||||
|
}
|
||||||
|
downPath := "down.file" + time.Now().Format(time.RFC3339)
|
||||||
|
_, err = client.Object.Download(context.Background(), "test.go.download", downPath, opt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Object.Upload returned error: %v", err)
|
||||||
|
}
|
||||||
|
os.Remove(downPath)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user