|
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) { |
|
|
|
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 |
|
} |
|
|
|
|
|
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 |
|
|
|
|
|
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)) |
|
} |
|
|
|
|
|
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 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
var _ driver.Driver = (*Lark)(nil) |
|
|