mirror of https://github.com/Xhofe/alist
* fix(aliyundrive_open): refresh upload url for large files * fix(aliyundrive_open): retry upload on url expiry * fix(aliyundrive_open): ignore 409 error * feat(aliyundrive): cleanup upload retry logic * feat(util): add multireadable io utility * feat(aliyundrive_open): make upload fully stream * feat(aliyundrive_open): refresh upload url every 20 puts * fix(aliyundrive_open): part info panic * chore: change refresh upload url strategy --------- Co-authored-by: Andy Hsu <i@nn.ci>pull/4027/head
parent
3b07c72f88
commit
4ec274e748
|
@ -5,7 +5,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
|
@ -148,11 +148,7 @@ func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream mode
|
||||||
count := 1
|
count := 1
|
||||||
if stream.GetSize() > DEFAULT {
|
if stream.GetSize() > DEFAULT {
|
||||||
count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
|
count = int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
|
||||||
partInfoList := make([]base.Json, 0, count)
|
createData["part_info_list"] = makePartInfos(count)
|
||||||
for i := 1; i <= count; i++ {
|
|
||||||
partInfoList = append(partInfoList, base.Json{"part_number": i})
|
|
||||||
}
|
|
||||||
createData["part_info_list"] = partInfoList
|
|
||||||
}
|
}
|
||||||
var createResp CreateResp
|
var createResp CreateResp
|
||||||
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/adrive/v1.0/openFile/create", http.MethodPost, func(req *resty.Request) {
|
||||||
|
@ -162,28 +158,26 @@ func (d *AliyundriveOpen) Put(ctx context.Context, dstDir model.Obj, stream mode
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 2. upload
|
// 2. upload
|
||||||
for i, partInfo := range createResp.PartInfoList {
|
preTime := time.Now()
|
||||||
|
for i := 1; i <= len(createResp.PartInfoList); i++ {
|
||||||
if utils.IsCanceled(ctx) {
|
if utils.IsCanceled(ctx) {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
uploadUrl := partInfo.UploadUrl
|
err = d.uploadPart(ctx, i, count, utils.NewMultiReadable(io.LimitReader(stream, DEFAULT)), &createResp, true)
|
||||||
if d.InternalUpload {
|
|
||||||
//Replace a known public Host with an internal Host
|
|
||||||
uploadUrl = strings.ReplaceAll(uploadUrl, "https://cn-beijing-data.aliyundrive.net/", "http://ccp-bj29-bj-1592982087.oss-cn-beijing-internal.aliyuncs.com/")
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("PUT", uploadUrl, io.LimitReader(stream, DEFAULT))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req = req.WithContext(ctx)
|
|
||||||
res, err := base.HttpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
up(i * 100 / count)
|
up(i * 100 / count)
|
||||||
}
|
}
|
||||||
|
// refresh upload url if 50 minutes passed
|
||||||
|
if time.Since(preTime) > 50*time.Minute {
|
||||||
|
createResp.PartInfoList, err = d.getUploadUrl(count, createResp.FileId, createResp.UploadId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
preTime = time.Now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 3. complete
|
// 3. complete
|
||||||
_, err = d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
|
_, err = d.request("/adrive/v1.0/openFile/complete", http.MethodPost, func(req *resty.Request) {
|
||||||
|
|
|
@ -45,6 +45,14 @@ func fileToObj(f File) *model.ObjThumb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PartInfo struct {
|
||||||
|
Etag interface{} `json:"etag"`
|
||||||
|
PartNumber int `json:"part_number"`
|
||||||
|
PartSize interface{} `json:"part_size"`
|
||||||
|
UploadUrl string `json:"upload_url"`
|
||||||
|
ContentType string `json:"content_type"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateResp struct {
|
type CreateResp struct {
|
||||||
//Type string `json:"type"`
|
//Type string `json:"type"`
|
||||||
//ParentFileId string `json:"parent_file_id"`
|
//ParentFileId string `json:"parent_file_id"`
|
||||||
|
@ -57,11 +65,5 @@ type CreateResp struct {
|
||||||
UploadId string `json:"upload_id"`
|
UploadId string `json:"upload_id"`
|
||||||
//Location string `json:"location"`
|
//Location string `json:"location"`
|
||||||
RapidUpload bool `json:"rapid_upload"`
|
RapidUpload bool `json:"rapid_upload"`
|
||||||
PartInfoList []struct {
|
PartInfoList []PartInfo `json:"part_info_list"`
|
||||||
Etag interface{} `json:"etag"`
|
|
||||||
PartNumber int `json:"part_number"`
|
|
||||||
PartSize interface{} `json:"part_size"`
|
|
||||||
UploadUrl string `json:"upload_url"`
|
|
||||||
ContentType string `json:"content_type"`
|
|
||||||
} `json:"part_info_list"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package aliyundrive_open
|
package aliyundrive_open
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -106,3 +109,59 @@ func (d *AliyundriveOpen) getFiles(fileId string) ([]File, error) {
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makePartInfos(size int) []base.Json {
|
||||||
|
partInfoList := make([]base.Json, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
partInfoList[i] = base.Json{"part_number": 1 + i}
|
||||||
|
}
|
||||||
|
return partInfoList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AliyundriveOpen) getUploadUrl(count int, fileId, uploadId string) ([]PartInfo, error) {
|
||||||
|
partInfoList := makePartInfos(count)
|
||||||
|
var resp CreateResp
|
||||||
|
_, err := d.request("/adrive/v1.0/openFile/getUploadUrl", http.MethodPost, func(req *resty.Request) {
|
||||||
|
req.SetBody(base.Json{
|
||||||
|
"drive_id": d.DriveId,
|
||||||
|
"file_id": fileId,
|
||||||
|
"part_info_list": partInfoList,
|
||||||
|
"upload_id": uploadId,
|
||||||
|
}).SetResult(&resp)
|
||||||
|
})
|
||||||
|
return resp.PartInfoList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *AliyundriveOpen) uploadPart(ctx context.Context, i, count int, reader *utils.MultiReadable, resp *CreateResp, retry bool) error {
|
||||||
|
partInfo := resp.PartInfoList[i-1]
|
||||||
|
uploadUrl := partInfo.UploadUrl
|
||||||
|
if d.InternalUpload {
|
||||||
|
uploadUrl = strings.ReplaceAll(uploadUrl, "https://cn-beijing-data.aliyundrive.net/", "http://ccp-bj29-bj-1592982087.oss-cn-beijing-internal.aliyuncs.com/")
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("PUT", uploadUrl, reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
res, err := base.HttpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if retry {
|
||||||
|
reader.Reset()
|
||||||
|
return d.uploadPart(ctx, i, count, reader, resp, false)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res.Body.Close()
|
||||||
|
if retry && res.StatusCode == http.StatusForbidden {
|
||||||
|
resp.PartInfoList, err = d.getUploadUrl(count, resp.FileId, resp.UploadId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader.Reset()
|
||||||
|
return d.uploadPart(ctx, i, count, reader, resp, false)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusConflict {
|
||||||
|
return fmt.Errorf("upload status: %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
@ -91,3 +92,46 @@ func NewReadCloser(reader io.Reader, close CloseFunc) io.ReadCloser {
|
||||||
func NewLimitReadCloser(reader io.Reader, close CloseFunc, limit int64) io.ReadCloser {
|
func NewLimitReadCloser(reader io.Reader, close CloseFunc, limit int64) io.ReadCloser {
|
||||||
return NewReadCloser(io.LimitReader(reader, limit), close)
|
return NewReadCloser(io.LimitReader(reader, limit), close)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MultiReadable struct {
|
||||||
|
originReader io.Reader
|
||||||
|
reader io.Reader
|
||||||
|
cache *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultiReadable(reader io.Reader) *MultiReadable {
|
||||||
|
return &MultiReadable{
|
||||||
|
originReader: reader,
|
||||||
|
reader: reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MultiReadable) Read(p []byte) (int, error) {
|
||||||
|
n, err := mr.reader.Read(p)
|
||||||
|
if _, ok := mr.reader.(io.Seeker); !ok && n > 0 {
|
||||||
|
if mr.cache == nil {
|
||||||
|
mr.cache = &bytes.Buffer{}
|
||||||
|
}
|
||||||
|
mr.cache.Write(p[:n])
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MultiReadable) Reset() error {
|
||||||
|
if seeker, ok := mr.reader.(io.Seeker); ok {
|
||||||
|
_, err := seeker.Seek(0, io.SeekStart)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if mr.cache != nil && mr.cache.Len() > 0 {
|
||||||
|
mr.reader = io.MultiReader(mr.cache, mr.reader)
|
||||||
|
mr.cache = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MultiReadable) Close() error {
|
||||||
|
if closer, ok := mr.originReader.(io.Closer); ok {
|
||||||
|
return closer.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue