From 5d0668b00bcc4db4e69409fff7bf9a81b4ca0cbb Mon Sep 17 00:00:00 2001 From: Noah Hsu Date: Sat, 3 Sep 2022 20:34:06 +0800 Subject: [PATCH] feat: add google_drive driver --- drivers/all.go | 1 + drivers/google_drive/driver.go | 150 +++++++++++++++++++++++++++++++++ drivers/google_drive/meta.go | 29 +++++++ drivers/google_drive/types.go | 55 ++++++++++++ drivers/google_drive/util.go | 97 +++++++++++++++++++++ internal/model/stream.go | 4 +- 6 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 drivers/google_drive/driver.go create mode 100644 drivers/google_drive/meta.go create mode 100644 drivers/google_drive/types.go create mode 100644 drivers/google_drive/util.go diff --git a/drivers/all.go b/drivers/all.go index df629d1d..cbd1f978 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -4,6 +4,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/123" _ "github.com/alist-org/alist/v3/drivers/aliyundrive" _ "github.com/alist-org/alist/v3/drivers/baidu_netdisk" + _ "github.com/alist-org/alist/v3/drivers/google_drive" _ "github.com/alist-org/alist/v3/drivers/local" _ "github.com/alist-org/alist/v3/drivers/onedrive" _ "github.com/alist-org/alist/v3/drivers/pikpak" diff --git a/drivers/google_drive/driver.go b/drivers/google_drive/driver.go new file mode 100644 index 00000000..1601b83c --- /dev/null +++ b/drivers/google_drive/driver.go @@ -0,0 +1,150 @@ +package google_drive + +import ( + "context" + "fmt" + "net/http" + + "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/utils" + "github.com/go-resty/resty/v2" +) + +type GoogleDrive struct { + model.Storage + Addition + AccessToken string +} + +func (d *GoogleDrive) Config() driver.Config { + return config +} + +func (d *GoogleDrive) GetAddition() driver.Additional { + return d.Addition +} + +func (d *GoogleDrive) Init(ctx context.Context, storage model.Storage) error { + d.Storage = storage + err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition) + if err != nil { + return err + } + return d.refreshToken() +} + +func (d *GoogleDrive) Drop(ctx context.Context) error { + return nil +} + +func (d *GoogleDrive) 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 + } + objs := make([]model.Obj, len(files)) + for i := 0; i < len(files); i++ { + objs[i] = fileToObj(files[i]) + } + return objs, nil +} + +//func (d *GoogleDrive) Get(ctx context.Context, path string) (model.Obj, error) { +// // this is optional +// return nil, errs.NotImplement +//} + +func (d *GoogleDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s?includeItemsFromAllDrives=true&supportsAllDrives=true", file.GetID()) + link := model.Link{ + URL: url + "&alt=media", + Header: http.Header{ + "Authorization": []string{"Bearer " + d.AccessToken}, + }, + } + return &link, nil +} + +func (d *GoogleDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + data := base.Json{ + "name": dirName, + "parents": []string{parentDir.GetID()}, + "mimeType": "application/vnd.google-apps.folder", + } + _, err := d.request("https://www.googleapis.com/drive/v3/files", http.MethodPost, func(req *resty.Request) { + req.SetBody(data) + }, nil) + return err +} + +func (d *GoogleDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + query := map[string]string{ + "addParents": dstDir.GetID(), + "removeParents": "root", + } + url := "https://www.googleapis.com/drive/v3/files/" + srcObj.GetID() + _, err := d.request(url, http.MethodPatch, func(req *resty.Request) { + req.SetQueryParams(query) + }, nil) + return err +} + +func (d *GoogleDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + data := base.Json{ + "name": newName, + } + url := "https://www.googleapis.com/drive/v3/files/" + srcObj.GetID() + _, err := d.request(url, http.MethodPatch, func(req *resty.Request) { + req.SetBody(data) + }, nil) + return err +} + +func (d *GoogleDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + return errs.NotSupport +} + +func (d *GoogleDrive) Remove(ctx context.Context, obj model.Obj) error { + url := "https://www.googleapis.com/drive/v3/files/" + obj.GetID() + _, err := d.request(url, http.MethodDelete, nil, nil) + return err +} + +func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + data := base.Json{ + "name": stream.GetName(), + "parents": []string{dstDir.GetID()}, + } + var e Error + url := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true" + res, err := base.NoRedirectClient.R().SetHeader("Authorization", "Bearer "+d.AccessToken). + SetError(&e).SetBody(data). + Post(url) + if err != nil { + return err + } + if e.Error.Code != 0 { + if e.Error.Code == 401 { + err = d.refreshToken() + if err != nil { + return err + } + return d.Put(ctx, dstDir, stream, up) + } + return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors) + } + putUrl := res.Header().Get("location") + _, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) { + req.SetBody(stream.GetReadCloser()) + }, nil) + return err +} + +func (d *GoogleDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { + return nil, errs.NotSupport +} + +var _ driver.Driver = (*GoogleDrive)(nil) diff --git a/drivers/google_drive/meta.go b/drivers/google_drive/meta.go new file mode 100644 index 00000000..71f5379c --- /dev/null +++ b/drivers/google_drive/meta.go @@ -0,0 +1,29 @@ +package google_drive + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + driver.RootFolderID + RefreshToken string `json:"refresh_token" required:"true"` + OrderBy string `json:"order_by" type:"string" help:"such as: folder,name,modifiedTime"` + OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"` + ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"` + ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"` +} + +var config = driver.Config{ + Name: "GoogleDrive", + OnlyProxy: true, + DefaultRoot: "root", +} + +func New() driver.Driver { + return &GoogleDrive{} +} + +func init() { + op.RegisterDriver(config, New) +} diff --git a/drivers/google_drive/types.go b/drivers/google_drive/types.go new file mode 100644 index 00000000..f6074a0e --- /dev/null +++ b/drivers/google_drive/types.go @@ -0,0 +1,55 @@ +package google_drive + +import ( + "strconv" + "time" + + "github.com/alist-org/alist/v3/internal/model" +) + +type TokenError struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +type Files struct { + NextPageToken string `json:"nextPageToken"` + Files []File `json:"files"` +} + +type File struct { + Id string `json:"id"` + Name string `json:"name"` + MimeType string `json:"mimeType"` + ModifiedTime time.Time `json:"modifiedTime"` + Size string `json:"size"` + ThumbnailLink string `json:"thumbnailLink"` +} + +func fileToObj(f File) *model.ObjThumb { + size, _ := strconv.ParseInt(f.Size, 10, 64) + return &model.ObjThumb{ + Object: model.Object{ + ID: f.Id, + Name: f.Name, + Size: size, + Modified: time.Time{}, + IsFolder: false, + }, + Thumbnail: model.Thumbnail{}, + } +} + +type Error struct { + Error struct { + Errors []struct { + Domain string `json:"domain"` + Reason string `json:"reason"` + Message string `json:"message"` + LocationType string `json:"location_type"` + Location string `json:"location"` + } + Code int `json:"code"` + Message string `json:"message"` + } `json:"error"` +} diff --git a/drivers/google_drive/util.go b/drivers/google_drive/util.go new file mode 100644 index 00000000..eaae584f --- /dev/null +++ b/drivers/google_drive/util.go @@ -0,0 +1,97 @@ +package google_drive + +import ( + "fmt" + "net/http" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" +) + +// do others that not defined in Driver interface + +func (d *GoogleDrive) refreshToken() error { + url := "https://www.googleapis.com/oauth2/v4/token" + var resp base.TokenResp + var e TokenError + res, err := base.RestyClient.R().SetResult(&resp).SetError(&e). + SetFormData(map[string]string{ + "client_id": d.ClientID, + "client_secret": d.ClientSecret, + "refresh_token": d.RefreshToken, + "grant_type": "refresh_token", + }).Post(url) + if err != nil { + return err + } + log.Debug(res.String()) + if e.Error != "" { + return fmt.Errorf(e.Error) + } + d.AccessToken = resp.AccessToken + return nil +} + +func (d *GoogleDrive) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + req := base.RestyClient.R() + req.SetHeader("Authorization", "Bearer "+d.AccessToken) + req.SetQueryParam("includeItemsFromAllDrives", "true") + req.SetQueryParam("supportsAllDrives", "true") + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(resp) + } + var e Error + req.SetError(&e) + res, err := req.Execute(method, url) + if err != nil { + return nil, err + } + if e.Error.Code != 0 { + if e.Error.Code == 401 { + err = d.refreshToken() + if err != nil { + return nil, err + } + return d.request(url, method, callback, resp) + } + return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors) + } + return res.Body(), nil +} + +func (d *GoogleDrive) getFiles(id string) ([]File, error) { + pageToken := "first" + res := make([]File, 0) + for pageToken != "" { + if pageToken == "first" { + pageToken = "" + } + var resp Files + orderBy := "folder,name,modifiedTime desc" + if d.OrderBy != "" { + orderBy = d.OrderBy + " " + d.OrderDirection + } + query := map[string]string{ + "orderBy": orderBy, + "fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink),nextPageToken", + "pageSize": "1000", + "q": fmt.Sprintf("'%s' in parents and trashed = false", id), + //"includeItemsFromAllDrives": "true", + //"supportsAllDrives": "true", + "pageToken": pageToken, + } + _, err := d.request("https://www.googleapis.com/drive/v3/files", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(query) + }, &resp) + if err != nil { + return nil, err + } + pageToken = resp.NextPageToken + res = append(res, resp.Files...) + } + return res, nil +} diff --git a/internal/model/stream.go b/internal/model/stream.go index 490729c9..92d7ee2f 100644 --- a/internal/model/stream.go +++ b/internal/model/stream.go @@ -11,11 +11,11 @@ type FileStream struct { WebPutAsTask bool } -func (f FileStream) GetMimetype() string { +func (f *FileStream) GetMimetype() string { return f.Mimetype } -func (f FileStream) NeedStore() bool { +func (f *FileStream) NeedStore() bool { return f.WebPutAsTask }