diff --git a/drivers/lark.go b/drivers/lark.go new file mode 100644 index 00000000000..0fbbaddc09b --- /dev/null +++ b/drivers/lark.go @@ -0,0 +1,8 @@ +// +build linux darwin +// +build amd64 arm64 + +package drivers + +import ( + _ "github.com/alist-org/alist/v3/drivers/lark" +) \ No newline at end of file diff --git a/drivers/lark/driver.go b/drivers/lark/driver.go new file mode 100644 index 00000000000..6783aa5241d --- /dev/null +++ b/drivers/lark/driver.go @@ -0,0 +1,396 @@ +package lark + +import ( + "context" + "errors" + "fmt" + "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/ipfs/boxo/path" + 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" + "io" + "net/http" + "strconv" + "time" +) + +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 := path.SplitList(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: path.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 := path.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: path.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) diff --git a/drivers/lark/meta.go b/drivers/lark/meta.go new file mode 100644 index 00000000000..221345e222c --- /dev/null +++ b/drivers/lark/meta.go @@ -0,0 +1,36 @@ +package lark + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + // Usually one of two + driver.RootPath + // define other + AppId string `json:"app_id" type:"text" help:"app id"` + AppSecret string `json:"app_secret" type:"text" help:"app secret"` + ExternalMode bool `json:"external_mode" type:"bool" help:"external mode"` + TenantUrlPrefix string `json:"tenant_url_prefix" type:"text" help:"tenant url prefix"` +} + +var config = driver.Config{ + Name: "Lark", + LocalSort: false, + OnlyLocal: false, + OnlyProxy: false, + NoCache: false, + NoUpload: false, + NeedMs: false, + DefaultRoot: "/", + CheckStatus: false, + Alert: "", + NoOverwriteUpload: true, +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &Lark{} + }) +} diff --git a/drivers/lark/types.go b/drivers/lark/types.go new file mode 100644 index 00000000000..3ebefd556dc --- /dev/null +++ b/drivers/lark/types.go @@ -0,0 +1,32 @@ +package lark + +import ( + "context" + "github.com/Xhofe/go-cache" + "time" +) + +type TokenCache struct { + cache.ICache[string] +} + +func (t *TokenCache) Set(_ context.Context, key string, value string, expireTime time.Duration) error { + t.ICache.Set(key, value, cache.WithEx[string](expireTime)) + + return nil +} + +func (t *TokenCache) Get(_ context.Context, key string) (string, error) { + v, ok := t.ICache.Get(key) + if ok { + return v, nil + } + + return "", nil +} + +func newTokenCache() *TokenCache { + c := cache.NewMemCache[string]() + + return &TokenCache{c} +} diff --git a/drivers/lark/util.go b/drivers/lark/util.go new file mode 100644 index 00000000000..8c6828bd176 --- /dev/null +++ b/drivers/lark/util.go @@ -0,0 +1,66 @@ +package lark + +import ( + "context" + "github.com/Xhofe/go-cache" + larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1" + log "github.com/sirupsen/logrus" + "path" + "time" +) + +const objTokenCacheDuration = 5 * time.Minute +const emptyFolderToken = "empty" + +var objTokenCache = cache.NewMemCache[string]() +var exOpts = cache.WithEx[string](objTokenCacheDuration) + +func (c *Lark) getObjToken(ctx context.Context, folderPath string) (string, bool) { + if token, ok := objTokenCache.Get(folderPath); ok { + return token, true + } + + dir, name := path.Split(folderPath) + // strip the last slash of dir if it exists + if len(dir) > 0 && dir[len(dir)-1] == '/' { + dir = dir[:len(dir)-1] + } + if name == "" { + return c.rootFolderToken, true + } + + var parentToken string + var found bool + parentToken, found = c.getObjToken(ctx, dir) + if !found { + return emptyFolderToken, false + } + + req := larkdrive.NewListFileReqBuilder().FolderToken(parentToken).Build() + resp, err := c.client.Drive.File.ListByIterator(ctx, req) + + if err != nil { + log.WithError(err).Error("failed to list files") + return emptyFolderToken, false + } + + var file *larkdrive.File + for { + found, file, err = resp.Next() + if !found { + break + } + + if err != nil { + log.WithError(err).Error("failed to get next file") + break + } + + if *file.Name == name { + objTokenCache.Set(folderPath, *file.Token, exOpts) + return *file.Token, true + } + } + + return emptyFolderToken, false +} diff --git a/go.mod b/go.mod index a994d7688f7..118c5a35001 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/charmbracelet/lipgloss v0.9.1 github.com/coreos/go-oidc v2.2.1+incompatible github.com/deckarep/golang-set/v2 v2.6.0 + github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 github.com/disintegration/imaging v1.6.2 github.com/djherbis/times v1.6.0 github.com/dlclark/regexp2 v1.10.0 @@ -36,6 +37,7 @@ require ( github.com/ipfs/go-ipfs-api v0.7.0 github.com/jlaffaye/ftp v0.2.0 github.com/json-iterator/go v1.1.12 + github.com/larksuite/oapi-sdk-go/v3 v3.2.5 github.com/maruel/natural v1.1.1 github.com/meilisearch/meilisearch-go v0.26.1 github.com/minio/sio v0.3.0 @@ -108,7 +110,6 @@ require ( github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect - github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/geoffgarside/ber v1.1.0 // indirect diff --git a/go.sum b/go.sum index 539151e660a..d8bc53dbb9a 100644 --- a/go.sum +++ b/go.sum @@ -184,6 +184,7 @@ github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= @@ -215,6 +216,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvki github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -261,6 +263,7 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg= github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= @@ -281,6 +284,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/larksuite/oapi-sdk-go/v3 v3.2.5 h1:MkmkfCHzvmi35EId9SeFPJMZ8bUsijnxwneAWHnnk0k= +github.com/larksuite/oapi-sdk-go/v3 v3.2.5/go.mod h1:ZEplY+kwuIrj/nqw5uSCINNATcH3KdxSN7y+UxYY5fI= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= @@ -483,6 +488,8 @@ github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25 h1:eDfebW/yfq9DtG9RO3K github.com/xhofe/gsync v0.0.0-20230917091818-2111ceb38a25/go.mod h1:fH4oNm5F9NfI5dLi0oIMtsLNKQOirUDbEMCIBb/7SU0= github.com/xhofe/tache v0.1.1 h1:O5QY4cVjIGELx3UGh6LbVAc18MWGXgRNQjMt72x6w/8= github.com/xhofe/tache v0.1.1/go.mod h1:iKumPFvywf30FRpAHHCt64G0JHLMzT0K+wyGedHsmTQ= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -497,6 +504,7 @@ golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -514,10 +522,14 @@ golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -534,6 +546,8 @@ golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= @@ -596,12 +610,16 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw= google.golang.org/api v0.134.0/go.mod h1:sjRL3UnjTx5UqNQS9EWr9N8p7xbHpy1k0XGRLCf3Spk= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=