|
package _139 |
|
|
|
import ( |
|
"context" |
|
"encoding/base64" |
|
"fmt" |
|
"io" |
|
"net/http" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"github.com/alist-org/alist/v3/drivers/base" |
|
"github.com/alist-org/alist/v3/internal/driver" |
|
"github.com/alist-org/alist/v3/internal/errs" |
|
"github.com/alist-org/alist/v3/internal/model" |
|
"github.com/alist-org/alist/v3/pkg/cron" |
|
"github.com/alist-org/alist/v3/pkg/utils" |
|
"github.com/google/uuid" |
|
log "github.com/sirupsen/logrus" |
|
) |
|
|
|
type Yun139 struct { |
|
model.Storage |
|
Addition |
|
cron *cron.Cron |
|
Account string |
|
} |
|
|
|
func (d *Yun139) Config() driver.Config { |
|
return config |
|
} |
|
|
|
func (d *Yun139) GetAddition() driver.Additional { |
|
return &d.Addition |
|
} |
|
|
|
func (d *Yun139) Init(ctx context.Context) error { |
|
if d.Authorization == "" { |
|
return fmt.Errorf("authorization is empty") |
|
} |
|
d.cron = cron.NewCron(time.Hour * 24 * 7) |
|
d.cron.Do(func() { |
|
err := d.refreshToken() |
|
if err != nil { |
|
log.Errorf("%+v", err) |
|
} |
|
}) |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
if len(d.Addition.RootFolderID) == 0 { |
|
d.RootFolderID = "/" |
|
} |
|
return nil |
|
case MetaPersonal: |
|
if len(d.Addition.RootFolderID) == 0 { |
|
d.RootFolderID = "root" |
|
} |
|
fallthrough |
|
case MetaFamily: |
|
decode, err := base64.StdEncoding.DecodeString(d.Authorization) |
|
if err != nil { |
|
return err |
|
} |
|
decodeStr := string(decode) |
|
splits := strings.Split(decodeStr, ":") |
|
if len(splits) < 2 { |
|
return fmt.Errorf("authorization is invalid, splits < 2") |
|
} |
|
d.Account = splits[1] |
|
_, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{ |
|
"qryUserExternInfoReq": base.Json{ |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
}, |
|
}, nil) |
|
return err |
|
default: |
|
return errs.NotImplement |
|
} |
|
} |
|
|
|
func (d *Yun139) Drop(ctx context.Context) error { |
|
if d.cron != nil { |
|
d.cron.Stop() |
|
} |
|
return nil |
|
} |
|
|
|
func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
return d.personalGetFiles(dir.GetID()) |
|
case MetaPersonal: |
|
return d.getFiles(dir.GetID()) |
|
case MetaFamily: |
|
return d.familyGetFiles(dir.GetID()) |
|
default: |
|
return nil, errs.NotImplement |
|
} |
|
} |
|
|
|
func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { |
|
var url string |
|
var err error |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
url, err = d.personalGetLink(file.GetID()) |
|
case MetaPersonal: |
|
fallthrough |
|
case MetaFamily: |
|
url, err = d.getLink(file.GetID()) |
|
default: |
|
return nil, errs.NotImplement |
|
} |
|
if err != nil { |
|
return nil, err |
|
} |
|
return &model.Link{URL: url}, nil |
|
} |
|
|
|
func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { |
|
var err error |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
data := base.Json{ |
|
"parentFileId": parentDir.GetID(), |
|
"name": dirName, |
|
"description": "", |
|
"type": "folder", |
|
"fileRenameMode": "force_rename", |
|
} |
|
pathname := "/hcy/file/create" |
|
_, err = d.personalPost(pathname, data, nil) |
|
case MetaPersonal: |
|
data := base.Json{ |
|
"createCatalogExtReq": base.Json{ |
|
"parentCatalogID": parentDir.GetID(), |
|
"newCatalogName": dirName, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
}, |
|
} |
|
pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt" |
|
_, err = d.post(pathname, data, nil) |
|
case MetaFamily: |
|
cataID := parentDir.GetID() |
|
path := cataID |
|
data := base.Json{ |
|
"cloudID": d.CloudID, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
"docLibName": dirName, |
|
"path": path, |
|
} |
|
pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc" |
|
_, err = d.post(pathname, data, nil) |
|
default: |
|
err = errs.NotImplement |
|
} |
|
return err |
|
} |
|
|
|
func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
data := base.Json{ |
|
"fileIds": []string{srcObj.GetID()}, |
|
"toParentFileId": dstDir.GetID(), |
|
} |
|
pathname := "/hcy/file/batchMove" |
|
_, err := d.personalPost(pathname, data, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return srcObj, nil |
|
case MetaPersonal: |
|
var contentInfoList []string |
|
var catalogInfoList []string |
|
if srcObj.IsDir() { |
|
catalogInfoList = append(catalogInfoList, srcObj.GetID()) |
|
} else { |
|
contentInfoList = append(contentInfoList, srcObj.GetID()) |
|
} |
|
data := base.Json{ |
|
"createBatchOprTaskReq": base.Json{ |
|
"taskType": 3, |
|
"actionType": "304", |
|
"taskInfo": base.Json{ |
|
"contentInfoList": contentInfoList, |
|
"catalogInfoList": catalogInfoList, |
|
"newCatalogID": dstDir.GetID(), |
|
}, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
}, |
|
} |
|
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" |
|
_, err := d.post(pathname, data, nil) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return srcObj, nil |
|
default: |
|
return nil, errs.NotImplement |
|
} |
|
} |
|
|
|
func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error { |
|
var err error |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
data := base.Json{ |
|
"fileId": srcObj.GetID(), |
|
"name": newName, |
|
"description": "", |
|
} |
|
pathname := "/hcy/file/update" |
|
_, err = d.personalPost(pathname, data, nil) |
|
case MetaPersonal: |
|
var data base.Json |
|
var pathname string |
|
if srcObj.IsDir() { |
|
data = base.Json{ |
|
"catalogID": srcObj.GetID(), |
|
"catalogName": newName, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
} |
|
pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo" |
|
} else { |
|
data = base.Json{ |
|
"contentID": srcObj.GetID(), |
|
"contentName": newName, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
} |
|
pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo" |
|
} |
|
_, err = d.post(pathname, data, nil) |
|
default: |
|
err = errs.NotImplement |
|
} |
|
return err |
|
} |
|
|
|
func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { |
|
var err error |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
data := base.Json{ |
|
"fileIds": []string{srcObj.GetID()}, |
|
"toParentFileId": dstDir.GetID(), |
|
} |
|
pathname := "/hcy/file/batchCopy" |
|
_, err := d.personalPost(pathname, data, nil) |
|
return err |
|
case MetaPersonal: |
|
var contentInfoList []string |
|
var catalogInfoList []string |
|
if srcObj.IsDir() { |
|
catalogInfoList = append(catalogInfoList, srcObj.GetID()) |
|
} else { |
|
contentInfoList = append(contentInfoList, srcObj.GetID()) |
|
} |
|
data := base.Json{ |
|
"createBatchOprTaskReq": base.Json{ |
|
"taskType": 3, |
|
"actionType": 309, |
|
"taskInfo": base.Json{ |
|
"contentInfoList": contentInfoList, |
|
"catalogInfoList": catalogInfoList, |
|
"newCatalogID": dstDir.GetID(), |
|
}, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
}, |
|
} |
|
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" |
|
_, err = d.post(pathname, data, nil) |
|
default: |
|
err = errs.NotImplement |
|
} |
|
return err |
|
} |
|
|
|
func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error { |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
data := base.Json{ |
|
"fileIds": []string{obj.GetID()}, |
|
} |
|
pathname := "/hcy/recyclebin/batchTrash" |
|
_, err := d.personalPost(pathname, data, nil) |
|
return err |
|
case MetaPersonal: |
|
fallthrough |
|
case MetaFamily: |
|
return errs.NotImplement |
|
log.Warn("==========================================") |
|
var contentInfoList []string |
|
var catalogInfoList []string |
|
cataID := obj.GetID() |
|
path := "" |
|
if strings.Contains(cataID, "/") { |
|
lastSlashIndex := strings.LastIndex(cataID, "/") |
|
path = cataID[0:lastSlashIndex] |
|
cataID = cataID[lastSlashIndex+1:] |
|
} |
|
|
|
if obj.IsDir() { |
|
catalogInfoList = append(catalogInfoList, cataID) |
|
} else { |
|
contentInfoList = append(contentInfoList, cataID) |
|
} |
|
data := base.Json{ |
|
"createBatchOprTaskReq": base.Json{ |
|
"taskType": 2, |
|
"actionType": 201, |
|
"taskInfo": base.Json{ |
|
"newCatalogID": "", |
|
"contentInfoList": contentInfoList, |
|
"catalogInfoList": catalogInfoList, |
|
}, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
}, |
|
} |
|
pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask" |
|
if d.isFamily() { |
|
data = base.Json{ |
|
"taskType": 2, |
|
"sourceCloudID": d.CloudID, |
|
"sourceCatalogType": 1002, |
|
"path": path, |
|
"contentList": catalogInfoList, |
|
"catalogList": contentInfoList, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
} |
|
pathname = "/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask" |
|
} |
|
_, err := d.post(pathname, data, nil) |
|
return err |
|
default: |
|
return errs.NotImplement |
|
} |
|
} |
|
|
|
const ( |
|
_ = iota |
|
KB = 1 << (10 * iota) |
|
MB |
|
GB |
|
TB |
|
) |
|
|
|
func getPartSize(size int64) int64 { |
|
|
|
if size/GB > 30 { |
|
return 512 * MB |
|
} |
|
return 350 * MB |
|
} |
|
|
|
|
|
|
|
func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
var err error |
|
fullHash := stream.GetHash().GetHash(utils.SHA256) |
|
if len(fullHash) <= 0 { |
|
tmpF, err := stream.CacheFullInTempFile() |
|
if err != nil { |
|
return err |
|
} |
|
fullHash, err = utils.HashFile(utils.SHA256, tmpF) |
|
if err != nil { |
|
return err |
|
} |
|
} |
|
|
|
partInfos := []PartInfo{} |
|
var partSize = getPartSize(stream.GetSize()) |
|
part := (stream.GetSize() + partSize - 1) / partSize |
|
if part == 0 { |
|
part = 1 |
|
} |
|
for i := int64(0); i < part; i++ { |
|
if utils.IsCanceled(ctx) { |
|
return ctx.Err() |
|
} |
|
|
|
start := i * partSize |
|
byteSize := stream.GetSize() - start |
|
if byteSize > partSize { |
|
byteSize = partSize |
|
} |
|
partNumber := i + 1 |
|
partInfo := PartInfo{ |
|
PartNumber: partNumber, |
|
PartSize: byteSize, |
|
ParallelHashCtx: ParallelHashCtx{ |
|
PartOffset: start, |
|
}, |
|
} |
|
partInfos = append(partInfos, partInfo) |
|
} |
|
|
|
|
|
data := base.Json{ |
|
"contentHash": fullHash, |
|
"contentHashAlgorithm": "SHA256", |
|
"contentType": "application/octet-stream", |
|
"parallelUpload": false, |
|
"partInfos": partInfos, |
|
"size": stream.GetSize(), |
|
"parentFileId": dstDir.GetID(), |
|
"name": stream.GetName(), |
|
"type": "file", |
|
"fileRenameMode": "auto_rename", |
|
} |
|
pathname := "/hcy/file/create" |
|
var resp PersonalUploadResp |
|
_, err = d.personalPost(pathname, data, &resp) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if resp.Data.Exist || resp.Data.RapidUpload { |
|
return nil |
|
} |
|
|
|
|
|
p := driver.NewProgress(stream.GetSize(), up) |
|
|
|
|
|
|
|
|
|
for index, partInfo := range resp.Data.PartInfos { |
|
|
|
int64Index := int64(index) |
|
start := int64Index * partSize |
|
byteSize := stream.GetSize() - start |
|
if byteSize > partSize { |
|
byteSize = partSize |
|
} |
|
|
|
retry := 2 |
|
for attempt := 0; attempt <= retry; attempt++ { |
|
limitReader := io.LimitReader(stream, byteSize) |
|
|
|
r := io.TeeReader(limitReader, p) |
|
req, err := http.NewRequest("PUT", partInfo.UploadUrl, r) |
|
if err != nil { |
|
return err |
|
} |
|
req = req.WithContext(ctx) |
|
req.Header.Set("Content-Type", "application/octet-stream") |
|
req.Header.Set("Content-Length", fmt.Sprint(byteSize)) |
|
req.Header.Set("Origin", "https://yun.139.com") |
|
req.Header.Set("Referer", "https://yun.139.com/") |
|
req.ContentLength = byteSize |
|
|
|
res, err := base.HttpClient.Do(req) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
_ = res.Body.Close() |
|
log.Debugf("%+v", res) |
|
if res.StatusCode != http.StatusOK { |
|
if res.StatusCode == http.StatusRequestTimeout && attempt < retry{ |
|
log.Warn("服务器返回 408,尝试重试...") |
|
continue |
|
}else{ |
|
return fmt.Errorf("unexpected status code: %d", res.StatusCode) |
|
} |
|
} |
|
break |
|
} |
|
} |
|
|
|
data = base.Json{ |
|
"contentHash": fullHash, |
|
"contentHashAlgorithm": "SHA256", |
|
"fileId": resp.Data.FileId, |
|
"uploadId": resp.Data.UploadId, |
|
} |
|
_, err = d.personalPost("/hcy/file/complete", data, nil) |
|
if err != nil { |
|
return err |
|
} |
|
return nil |
|
case MetaPersonal: |
|
fallthrough |
|
case MetaFamily: |
|
data := base.Json{ |
|
"manualRename": 2, |
|
"operation": 0, |
|
"fileCount": 1, |
|
"totalSize": 0, |
|
"uploadContentList": []base.Json{{ |
|
"contentName": stream.GetName(), |
|
"contentSize": stream.GetSize(), |
|
|
|
}}, |
|
"parentCatalogID": dstDir.GetID(), |
|
"newCatalogName": "", |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
} |
|
pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest" |
|
if d.isFamily() { |
|
cataID := dstDir.GetID() |
|
path := cataID |
|
seqNo, _ := uuid.NewUUID() |
|
data = base.Json{ |
|
"cloudID": d.CloudID, |
|
"path": path, |
|
"operation": 0, |
|
"cloudType": 1, |
|
"catalogType": 3, |
|
"manualRename": 2, |
|
"fileCount": 1, |
|
"totalSize": stream.GetSize(), |
|
"uploadContentList": []base.Json{{ |
|
"contentName": stream.GetName(), |
|
"contentSize": stream.GetSize(), |
|
}}, |
|
"seqNo": seqNo, |
|
"commonAccountInfo": base.Json{ |
|
"account": d.Account, |
|
"accountType": 1, |
|
}, |
|
} |
|
pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL" |
|
|
|
} |
|
var resp UploadResp |
|
_, err := d.post(pathname, data, &resp) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
p := driver.NewProgress(stream.GetSize(), up) |
|
|
|
var partSize = getPartSize(stream.GetSize()) |
|
|
|
part := (stream.GetSize() + partSize - 1) / partSize |
|
if part == 0 { |
|
part = 1 |
|
} |
|
for i := int64(0); i < part; i++ { |
|
if utils.IsCanceled(ctx) { |
|
return ctx.Err() |
|
} |
|
|
|
start := i * partSize |
|
byteSize := stream.GetSize() - start |
|
if byteSize > partSize { |
|
byteSize = partSize |
|
} |
|
|
|
retry := 2 |
|
for attempt := 0; attempt <= retry; attempt++ { |
|
limitReader := io.LimitReader(stream, byteSize) |
|
|
|
r := io.TeeReader(limitReader, p) |
|
req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
req = req.WithContext(ctx) |
|
req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName())) |
|
req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10)) |
|
req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1)) |
|
req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID) |
|
req.Header.Set("rangeType", "0") |
|
req.ContentLength = byteSize |
|
|
|
res, err := base.HttpClient.Do(req) |
|
if err != nil { |
|
return err |
|
} |
|
_ = res.Body.Close() |
|
log.Debugf("%+v", res) |
|
if res.StatusCode != http.StatusOK { |
|
if res.StatusCode == http.StatusRequestTimeout && attempt < retry { |
|
log.Warn("服务器返回 408,尝试重试...") |
|
continue |
|
}else{ |
|
return fmt.Errorf("unexpected status code: %d", res.StatusCode) |
|
} |
|
} |
|
break |
|
} |
|
} |
|
|
|
return nil |
|
default: |
|
return errs.NotImplement |
|
} |
|
} |
|
|
|
func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { |
|
switch d.Addition.Type { |
|
case MetaPersonalNew: |
|
var resp base.Json |
|
var uri string |
|
data := base.Json{ |
|
"category": "video", |
|
"fileId": args.Obj.GetID(), |
|
} |
|
switch args.Method { |
|
case "video_preview": |
|
uri = "/hcy/videoPreview/getPreviewInfo" |
|
default: |
|
return nil, errs.NotSupport |
|
} |
|
_, err := d.personalPost(uri, data, &resp) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return resp["data"], nil |
|
default: |
|
return nil, errs.NotImplement |
|
} |
|
} |
|
|
|
var _ driver.Driver = (*Yun139)(nil) |
|
|