Spaces:
Paused
Paused
| package lark | |
| import ( | |
| "context" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "github.com/alist-org/alist/v3/internal/driver" | |
| "github.com/alist-org/alist/v3/internal/errs" | |
| "github.com/alist-org/alist/v3/internal/model" | |
| lark "github.com/larksuite/oapi-sdk-go/v3" | |
| larkcore "github.com/larksuite/oapi-sdk-go/v3/core" | |
| larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1" | |
| "golang.org/x/time/rate" | |
| ) | |
| type Lark struct { | |
| model.Storage | |
| Addition | |
| client *lark.Client | |
| rootFolderToken string | |
| } | |
| func (c *Lark) Config() driver.Config { | |
| return config | |
| } | |
| func (c *Lark) GetAddition() driver.Additional { | |
| return &c.Addition | |
| } | |
| func (c *Lark) Init(ctx context.Context) error { | |
| c.client = lark.NewClient(c.AppId, c.AppSecret, lark.WithTokenCache(newTokenCache())) | |
| paths := strings.Split(c.RootFolderPath, "/") | |
| token := "" | |
| var ok bool | |
| var file *larkdrive.File | |
| for _, p := range paths { | |
| if p == "" { | |
| token = "" | |
| continue | |
| } | |
| resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build()) | |
| if err != nil { | |
| return err | |
| } | |
| for { | |
| ok, file, err = resp.Next() | |
| if !ok { | |
| return errs.ObjectNotFound | |
| } | |
| if err != nil { | |
| return err | |
| } | |
| if *file.Type == "folder" && *file.Name == p { | |
| token = *file.Token | |
| break | |
| } | |
| } | |
| } | |
| c.rootFolderToken = token | |
| return nil | |
| } | |
| func (c *Lark) Drop(ctx context.Context) error { | |
| return nil | |
| } | |
| func (c *Lark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { | |
| token, ok := c.getObjToken(ctx, dir.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| if token == emptyFolderToken { | |
| return nil, nil | |
| } | |
| resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| ok = false | |
| var file *larkdrive.File | |
| var res []model.Obj | |
| for { | |
| ok, file, err = resp.Next() | |
| if !ok { | |
| break | |
| } | |
| if err != nil { | |
| return nil, err | |
| } | |
| modifiedUnix, _ := strconv.ParseInt(*file.ModifiedTime, 10, 64) | |
| createdUnix, _ := strconv.ParseInt(*file.CreatedTime, 10, 64) | |
| f := model.Object{ | |
| ID: *file.Token, | |
| Path: strings.Join([]string{c.RootFolderPath, dir.GetPath(), *file.Name}, "/"), | |
| Name: *file.Name, | |
| Size: 0, | |
| Modified: time.Unix(modifiedUnix, 0), | |
| Ctime: time.Unix(createdUnix, 0), | |
| IsFolder: *file.Type == "folder", | |
| } | |
| res = append(res, &f) | |
| } | |
| return res, nil | |
| } | |
| func (c *Lark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { | |
| token, ok := c.getObjToken(ctx, file.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| resp, err := c.client.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{ | |
| AppID: c.AppId, | |
| AppSecret: c.AppSecret, | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !c.ExternalMode { | |
| accessToken := resp.TenantAccessToken | |
| url := fmt.Sprintf("https://open.feishu.cn/open-apis/drive/v1/files/%s/download", token) | |
| req, err := http.NewRequest(http.MethodGet, url, nil) | |
| if err != nil { | |
| return nil, err | |
| } | |
| req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) | |
| req.Header.Set("Range", "bytes=0-1") | |
| ar, err := http.DefaultClient.Do(req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if ar.StatusCode != http.StatusPartialContent { | |
| return nil, errors.New("failed to get download link") | |
| } | |
| return &model.Link{ | |
| URL: url, | |
| Header: http.Header{ | |
| "Authorization": []string{fmt.Sprintf("Bearer %s", accessToken)}, | |
| }, | |
| }, nil | |
| } else { | |
| url := strings.Join([]string{c.TenantUrlPrefix, "file", token}, "/") | |
| return &model.Link{ | |
| URL: url, | |
| }, nil | |
| } | |
| } | |
| func (c *Lark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { | |
| token, ok := c.getObjToken(ctx, parentDir.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| body, err := larkdrive.NewCreateFolderFilePathReqBodyBuilder().FolderToken(token).Name(dirName).Build() | |
| if err != nil { | |
| return nil, err | |
| } | |
| resp, err := c.client.Drive.File.CreateFolder(ctx, | |
| larkdrive.NewCreateFolderFileReqBuilder().Body(body).Build()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !resp.Success() { | |
| return nil, errors.New(resp.Error()) | |
| } | |
| return &model.Object{ | |
| ID: *resp.Data.Token, | |
| Path: strings.Join([]string{c.RootFolderPath, parentDir.GetPath(), dirName}, "/"), | |
| Name: dirName, | |
| Size: 0, | |
| IsFolder: true, | |
| }, nil | |
| } | |
| func (c *Lark) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { | |
| srcToken, ok := c.getObjToken(ctx, srcObj.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| req := larkdrive.NewMoveFileReqBuilder(). | |
| Body(larkdrive.NewMoveFileReqBodyBuilder(). | |
| Type("file"). | |
| FolderToken(dstDirToken). | |
| Build()).FileToken(srcToken). | |
| Build() | |
| // 发起请求 | |
| resp, err := c.client.Drive.File.Move(ctx, req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !resp.Success() { | |
| return nil, errors.New(resp.Error()) | |
| } | |
| return nil, nil | |
| } | |
| func (c *Lark) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { | |
| // TODO rename obj, optional | |
| return nil, errs.NotImplement | |
| } | |
| func (c *Lark) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) { | |
| srcToken, ok := c.getObjToken(ctx, srcObj.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| req := larkdrive.NewCopyFileReqBuilder(). | |
| Body(larkdrive.NewCopyFileReqBodyBuilder(). | |
| Name(srcObj.GetName()). | |
| Type("file"). | |
| FolderToken(dstDirToken). | |
| Build()).FileToken(srcToken). | |
| Build() | |
| // 发起请求 | |
| resp, err := c.client.Drive.File.Copy(ctx, req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !resp.Success() { | |
| return nil, errors.New(resp.Error()) | |
| } | |
| return nil, nil | |
| } | |
| func (c *Lark) Remove(ctx context.Context, obj model.Obj) error { | |
| token, ok := c.getObjToken(ctx, obj.GetPath()) | |
| if !ok { | |
| return errs.ObjectNotFound | |
| } | |
| req := larkdrive.NewDeleteFileReqBuilder(). | |
| FileToken(token). | |
| Type("file"). | |
| Build() | |
| // 发起请求 | |
| resp, err := c.client.Drive.File.Delete(ctx, req) | |
| if err != nil { | |
| return err | |
| } | |
| if !resp.Success() { | |
| return errors.New(resp.Error()) | |
| } | |
| return nil | |
| } | |
| var uploadLimit = rate.NewLimiter(rate.Every(time.Second), 5) | |
| func (c *Lark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { | |
| token, ok := c.getObjToken(ctx, dstDir.GetPath()) | |
| if !ok { | |
| return nil, errs.ObjectNotFound | |
| } | |
| // prepare | |
| req := larkdrive.NewUploadPrepareFileReqBuilder(). | |
| FileUploadInfo(larkdrive.NewFileUploadInfoBuilder(). | |
| FileName(stream.GetName()). | |
| ParentType(`explorer`). | |
| ParentNode(token). | |
| Size(int(stream.GetSize())). | |
| Build()). | |
| Build() | |
| // 发起请求 | |
| uploadLimit.Wait(ctx) | |
| resp, err := c.client.Drive.File.UploadPrepare(ctx, req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !resp.Success() { | |
| return nil, errors.New(resp.Error()) | |
| } | |
| uploadId := *resp.Data.UploadId | |
| blockSize := *resp.Data.BlockSize | |
| blockCount := *resp.Data.BlockNum | |
| // upload | |
| for i := 0; i < blockCount; i++ { | |
| length := int64(blockSize) | |
| if i == blockCount-1 { | |
| length = stream.GetSize() - int64(i*blockSize) | |
| } | |
| reader := io.LimitReader(stream, length) | |
| req := larkdrive.NewUploadPartFileReqBuilder(). | |
| Body(larkdrive.NewUploadPartFileReqBodyBuilder(). | |
| UploadId(uploadId). | |
| Seq(i). | |
| Size(int(length)). | |
| File(reader). | |
| Build()). | |
| Build() | |
| // 发起请求 | |
| uploadLimit.Wait(ctx) | |
| resp, err := c.client.Drive.File.UploadPart(ctx, req) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !resp.Success() { | |
| return nil, errors.New(resp.Error()) | |
| } | |
| up(float64(i) / float64(blockCount)) | |
| } | |
| //close | |
| closeReq := larkdrive.NewUploadFinishFileReqBuilder(). | |
| Body(larkdrive.NewUploadFinishFileReqBodyBuilder(). | |
| UploadId(uploadId). | |
| BlockNum(blockCount). | |
| Build()). | |
| Build() | |
| // 发起请求 | |
| closeResp, err := c.client.Drive.File.UploadFinish(ctx, closeReq) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !closeResp.Success() { | |
| return nil, errors.New(closeResp.Error()) | |
| } | |
| return &model.Object{ | |
| ID: *closeResp.Data.FileToken, | |
| }, nil | |
| } | |
| //func (d *Lark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { | |
| // return nil, errs.NotSupport | |
| //} | |
| var _ driver.Driver = (*Lark)(nil) | |