| package aliyundrive |
|
|
| import ( |
| "bytes" |
| "context" |
| "crypto/sha1" |
| "encoding/base64" |
| "encoding/hex" |
| "fmt" |
| "io" |
| "math" |
| "math/big" |
| "net/http" |
| "os" |
| "time" |
|
|
| "github.com/alist-org/alist/v3/internal/stream" |
|
|
| "github.com/alist-org/alist/v3/drivers/base" |
| "github.com/alist-org/alist/v3/internal/conf" |
| "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/go-resty/resty/v2" |
| log "github.com/sirupsen/logrus" |
| ) |
|
|
| type AliDrive struct { |
| model.Storage |
| Addition |
| AccessToken string |
| cron *cron.Cron |
| DriveId string |
| UserID string |
| } |
|
|
| func (d *AliDrive) Config() driver.Config { |
| return config |
| } |
|
|
| func (d *AliDrive) GetAddition() driver.Additional { |
| return &d.Addition |
| } |
|
|
| func (d *AliDrive) Init(ctx context.Context) error { |
| |
| |
| err := d.refreshToken() |
| if err != nil { |
| return err |
| } |
| |
| res, err, _ := d.request("https://api.alipan.com/v2/user/get", http.MethodPost, nil, nil) |
| if err != nil { |
| return err |
| } |
| d.DriveId = utils.Json.Get(res, "default_drive_id").ToString() |
| d.UserID = utils.Json.Get(res, "user_id").ToString() |
| d.cron = cron.NewCron(time.Hour * 2) |
| d.cron.Do(func() { |
| err := d.refreshToken() |
| if err != nil { |
| log.Errorf("%+v", err) |
| } |
| }) |
| if global.Has(d.UserID) { |
| return nil |
| } |
| |
| deviceID := utils.HashData(utils.SHA256, []byte(d.UserID)) |
| |
| privateKey, _ := NewPrivateKeyFromHex(deviceID) |
| state := State{ |
| privateKey: privateKey, |
| deviceID: deviceID, |
| } |
| |
| global.Store(d.UserID, &state) |
| |
| d.sign() |
| return nil |
| } |
|
|
| func (d *AliDrive) Drop(ctx context.Context) error { |
| if d.cron != nil { |
| d.cron.Stop() |
| } |
| return nil |
| } |
|
|
| func (d *AliDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { |
| files, err := d.getFiles(dir.GetID()) |
| if err != nil { |
| return nil, err |
| } |
| return utils.SliceConvert(files, func(src File) (model.Obj, error) { |
| return fileToObj(src), nil |
| }) |
| } |
|
|
| func (d *AliDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { |
| data := base.Json{ |
| "drive_id": d.DriveId, |
| "file_id": file.GetID(), |
| "expire_sec": 14400, |
| } |
| res, err, _ := d.request("https://api.alipan.com/v2/file/get_download_url", http.MethodPost, func(req *resty.Request) { |
| req.SetBody(data) |
| }, nil) |
| if err != nil { |
| return nil, err |
| } |
| return &model.Link{ |
| Header: http.Header{ |
| "Referer": []string{"https://www.alipan.com/"}, |
| }, |
| URL: utils.Json.Get(res, "url").ToString(), |
| }, nil |
| } |
|
|
| func (d *AliDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { |
| _, err, _ := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) { |
| req.SetBody(base.Json{ |
| "check_name_mode": "refuse", |
| "drive_id": d.DriveId, |
| "name": dirName, |
| "parent_file_id": parentDir.GetID(), |
| "type": "folder", |
| }) |
| }, nil) |
| return err |
| } |
|
|
| func (d *AliDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error { |
| err := d.batch(srcObj.GetID(), dstDir.GetID(), "/file/move") |
| return err |
| } |
|
|
| func (d *AliDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error { |
| _, err, _ := d.request("https://api.alipan.com/v3/file/update", http.MethodPost, func(req *resty.Request) { |
| req.SetBody(base.Json{ |
| "check_name_mode": "refuse", |
| "drive_id": d.DriveId, |
| "file_id": srcObj.GetID(), |
| "name": newName, |
| }) |
| }, nil) |
| return err |
| } |
|
|
| func (d *AliDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { |
| err := d.batch(srcObj.GetID(), dstDir.GetID(), "/file/copy") |
| return err |
| } |
|
|
| func (d *AliDrive) Remove(ctx context.Context, obj model.Obj) error { |
| _, err, _ := d.request("https://api.alipan.com/v2/recyclebin/trash", http.MethodPost, func(req *resty.Request) { |
| req.SetBody(base.Json{ |
| "drive_id": d.DriveId, |
| "file_id": obj.GetID(), |
| }) |
| }, nil) |
| return err |
| } |
|
|
| func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.FileStreamer, up driver.UpdateProgress) error { |
| file := stream.FileStream{ |
| Obj: streamer, |
| Reader: streamer, |
| Mimetype: streamer.GetMimetype(), |
| } |
| const DEFAULT int64 = 10485760 |
| var count = int(math.Ceil(float64(streamer.GetSize()) / float64(DEFAULT))) |
|
|
| partInfoList := make([]base.Json, 0, count) |
| for i := 1; i <= count; i++ { |
| partInfoList = append(partInfoList, base.Json{"part_number": i}) |
| } |
| reqBody := base.Json{ |
| "check_name_mode": "overwrite", |
| "drive_id": d.DriveId, |
| "name": file.GetName(), |
| "parent_file_id": dstDir.GetID(), |
| "part_info_list": partInfoList, |
| "size": file.GetSize(), |
| "type": "file", |
| } |
|
|
| var localFile *os.File |
| if fileStream, ok := file.Reader.(*stream.FileStream); ok { |
| localFile, _ = fileStream.Reader.(*os.File) |
| } |
| if d.RapidUpload { |
| buf := bytes.NewBuffer(make([]byte, 0, 1024)) |
| utils.CopyWithBufferN(buf, file, 1024) |
| reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes()) |
| if localFile != nil { |
| if _, err := localFile.Seek(0, io.SeekStart); err != nil { |
| return err |
| } |
| } else { |
| |
| file.Reader = struct { |
| io.Reader |
| io.Closer |
| }{ |
| Reader: io.MultiReader(buf, file), |
| Closer: &file, |
| } |
| } |
| } else { |
| reqBody["content_hash_name"] = "none" |
| reqBody["proof_version"] = "v1" |
| } |
|
|
| var resp UploadResp |
| _, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) { |
| req.SetBody(reqBody) |
| }, &resp) |
|
|
| if err != nil && e.Code != "PreHashMatched" { |
| return err |
| } |
|
|
| if d.RapidUpload && e.Code == "PreHashMatched" { |
| delete(reqBody, "pre_hash") |
| h := sha1.New() |
| if localFile != nil { |
| if err = utils.CopyWithCtx(ctx, h, localFile, 0, nil); err != nil { |
| return err |
| } |
| if _, err = localFile.Seek(0, io.SeekStart); err != nil { |
| return err |
| } |
| } else { |
| tempFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*") |
| if err != nil { |
| return err |
| } |
| defer func() { |
| _ = tempFile.Close() |
| _ = os.Remove(tempFile.Name()) |
| }() |
| if err = utils.CopyWithCtx(ctx, io.MultiWriter(tempFile, h), file, 0, nil); err != nil { |
| return err |
| } |
| localFile = tempFile |
| } |
| reqBody["content_hash"] = hex.EncodeToString(h.Sum(nil)) |
| reqBody["content_hash_name"] = "sha1" |
| reqBody["proof_version"] = "v1" |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| buf := make([]byte, 8) |
| r, _ := new(big.Int).SetString(utils.GetMD5EncodeStr(d.AccessToken)[:16], 16) |
| i := new(big.Int).SetInt64(file.GetSize()) |
| o := new(big.Int).SetInt64(0) |
| if file.GetSize() > 0 { |
| o = r.Mod(r, i) |
| } |
| n, _ := io.NewSectionReader(localFile, o.Int64(), 8).Read(buf[:8]) |
| reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n]) |
|
|
| _, err, e := d.request("https://api.alipan.com/adrive/v2/file/createWithFolders", http.MethodPost, func(req *resty.Request) { |
| req.SetBody(reqBody) |
| }, &resp) |
| if err != nil && e.Code != "PreHashMatched" { |
| return err |
| } |
| if resp.RapidUpload { |
| return nil |
| } |
| |
| if _, err = localFile.Seek(0, io.SeekStart); err != nil { |
| return err |
| } |
| file.Reader = localFile |
| } |
|
|
| for i, partInfo := range resp.PartInfoList { |
| if utils.IsCanceled(ctx) { |
| return ctx.Err() |
| } |
| url := partInfo.UploadUrl |
| if d.InternalUpload { |
| url = partInfo.InternalUploadUrl |
| } |
| req, err := http.NewRequest("PUT", url, io.LimitReader(file, DEFAULT)) |
| if err != nil { |
| return err |
| } |
| req = req.WithContext(ctx) |
| res, err := base.HttpClient.Do(req) |
| if err != nil { |
| return err |
| } |
| res.Body.Close() |
| if count > 0 { |
| up(float64(i) * 100 / float64(count)) |
| } |
| } |
| var resp2 base.Json |
| _, err, e = d.request("https://api.alipan.com/v2/file/complete", http.MethodPost, func(req *resty.Request) { |
| req.SetBody(base.Json{ |
| "drive_id": d.DriveId, |
| "file_id": resp.FileId, |
| "upload_id": resp.UploadId, |
| }) |
| }, &resp2) |
| if err != nil && e.Code != "PreHashMatched" { |
| return err |
| } |
| if resp2["file_id"] == resp.FileId { |
| return nil |
| } |
| return fmt.Errorf("%+v", resp2) |
| } |
|
|
| func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { |
| var resp base.Json |
| var url string |
| data := base.Json{ |
| "drive_id": d.DriveId, |
| "file_id": args.Obj.GetID(), |
| } |
| switch args.Method { |
| case "doc_preview": |
| url = "https://api.alipan.com/v2/file/get_office_preview_url" |
| data["access_token"] = d.AccessToken |
| case "video_preview": |
| url = "https://api.alipan.com/v2/file/get_video_preview_play_info" |
| data["category"] = "live_transcoding" |
| data["url_expire_sec"] = 14400 |
| default: |
| return nil, errs.NotSupport |
| } |
| _, err, _ := d.request(url, http.MethodPost, func(req *resty.Request) { |
| req.SetBody(data) |
| }, &resp) |
| if err != nil { |
| return nil, err |
| } |
| return resp, nil |
| } |
|
|
| var _ driver.Driver = (*AliDrive)(nil) |
|
|