diff --git a/drivers/alidrive.go b/drivers/alidrive.go index 90c3d957..b51dd559 100644 --- a/drivers/alidrive.go +++ b/drivers/alidrive.go @@ -112,7 +112,7 @@ type AliFile struct { Url string `json:"url"` } -func AliToFile(file AliFile) *model.File { +func (a AliDrive) FormatFile(file *AliFile) *model.File { f := &model.File{ Name: file.Name, Size: file.Size, @@ -148,7 +148,7 @@ func (a AliDrive) GetFiles(fileId string, account *model.Account) ([]AliFile, er SetResult(&resp). SetError(&e). SetHeader("authorization", "Bearer\t"+account.AccessToken). - SetBody(JsonStr(Json{ + SetBody(Json{ "drive_id": account.DriveId, "fields": "*", "image_thumbnail_process": "image/resize,w_400/format,jpeg", @@ -160,11 +160,20 @@ func (a AliDrive) GetFiles(fileId string, account *model.Account) ([]AliFile, er "parent_file_id": fileId, "video_thumbnail_process": "video/snapshot,t_0,f_jpg,ar_auto,w_300", "url_expire_sec": 14400, - })).Post("https://api.aliyundrive.com/v2/file/list") + }).Post("https://api.aliyundrive.com/v2/file/list") if err != nil { return nil, err } if e.Code != "" { + if e.Code == "AccessTokenInvalid" { + err = a.RefreshToken(account) + if err != nil { + return nil, err + } else { + _ = model.SaveAccount(*account) + return a.GetFiles(fileId, account) + } + } return nil, fmt.Errorf("%s", e.Message) } marker = resp.NextMarker @@ -202,12 +211,12 @@ func (a AliDrive) Path(path string, account *model.Account) (*model.File, []*mod if err == nil { file, ok := cache.(AliFile) if ok { - return AliToFile(file), nil, nil + return a.FormatFile(&file), nil, nil } else { files, _ := cache.([]AliFile) res := make([]*model.File, 0) for _, file = range files { - res = append(res, AliToFile(file)) + res = append(res, a.FormatFile(&file)) } return nil, res, nil } @@ -227,12 +236,12 @@ func (a AliDrive) Path(path string, account *model.Account) (*model.File, []*mod if file.Name == name { found = true if file.Type == "file" { - url,err := a.Link(path,account) + url, err := a.Link(path, account) if err != nil { return nil, nil, err } file.Url = url - return AliToFile(file), nil, nil + return a.FormatFile(&file), nil, nil } else { fileId = file.FileId break @@ -250,7 +259,7 @@ func (a AliDrive) Path(path string, account *model.Account) (*model.File, []*mod _ = conf.Cache.Set(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path), files, nil) res := make([]*model.File, 0) for _, file := range files { - res = append(res, AliToFile(file)) + res = append(res, a.FormatFile(&file)) } return nil, res, nil } @@ -275,34 +284,39 @@ func (a AliDrive) Link(path string, account *model.Account) (string, error) { return "", err } if e.Code != "" { + if e.Code == "AccessTokenInvalid" { + err = a.RefreshToken(account) + if err != nil { + return "", err + } else { + _ = model.SaveAccount(*account) + return a.Link(path, account) + } + } return "", fmt.Errorf("%s", e.Message) } return resp["url"].(string), nil } -type AliTokenResp struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` -} - -func AliRefreshToken(refresh string) (string, string, error) { +func (a AliDrive) RefreshToken(account *model.Account) error { url := "https://auth.aliyundrive.com/v2/account/token" - var resp AliTokenResp + var resp TokenResp var e AliRespError _, err := aliClient.R(). //ForceContentType("application/json"). - SetBody(JsonStr(Json{"refresh_token": refresh, "grant_type": "refresh_token"})). + SetBody(Json{"refresh_token": account.RefreshToken, "grant_type": "refresh_token"}). SetResult(&resp). SetError(&e). Post(url) if err != nil { - return "", "", err + return err } log.Debugf("%+v,%+v", resp, e) if e.Code != "" { - return "", "", fmt.Errorf("failed to refresh token: %s", e.Message) + return fmt.Errorf("failed to refresh token: %s", e.Message) } - return resp.RefreshToken, resp.AccessToken, nil + account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken + return nil } func (a AliDrive) Save(account *model.Account, old *model.Account) error { @@ -315,25 +329,24 @@ func (a AliDrive) Save(account *model.Account, old *model.Account) error { if account.Limit == 0 { account.Limit = 200 } - refresh, access, err := AliRefreshToken(account.RefreshToken) + err := a.RefreshToken(account) if err != nil { return err } var resp Json _, _ = aliClient.R().SetResult(&resp). SetBody("{}"). - SetHeader("authorization", "Bearer\t"+access). + SetHeader("authorization", "Bearer\t"+account.AccessToken). Post("https://api.aliyundrive.com/v2/user/get") log.Debugf("user info: %+v", resp) account.DriveId = resp["default_drive_id"].(string) - account.RefreshToken, account.AccessToken = refresh, access cronId, err := conf.Cron.AddFunc("@every 2h", func() { name := account.Name newAccount, ok := model.GetAccount(name) if !ok { return } - newAccount.RefreshToken, newAccount.AccessToken, err = AliRefreshToken(newAccount.RefreshToken) + err = a.RefreshToken(&newAccount) if err != nil { newAccount.Status = err.Error() } diff --git a/drivers/driver.go b/drivers/driver.go index 67b6e21e..dd9e1c9a 100644 --- a/drivers/driver.go +++ b/drivers/driver.go @@ -1,7 +1,6 @@ package drivers import ( - "encoding/json" "github.com/Xhofe/alist/model" "github.com/gofiber/fiber/v2" ) @@ -12,7 +11,7 @@ type Driver interface { Link(path string, account *model.Account) (string, error) Save(account *model.Account, old *model.Account) error Proxy(ctx *fiber.Ctx) - Preview(path string, account *model.Account) (interface{},error) + Preview(path string, account *model.Account) (interface{}, error) // TODO //MakeDir(path string, account *model.Account) error //Move(src string, des string, account *model.Account) error @@ -24,10 +23,16 @@ type Item struct { Name string `json:"name"` Label string `json:"label"` Type string `json:"type"` + Values string `json:"values"` Required bool `json:"required"` Description string `json:"description"` } +type TokenResp struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + var driversMap = map[string]Driver{} func RegisterDriver(name string, driver Driver) { @@ -48,8 +53,3 @@ func GetDrivers() map[string][]Item { } type Json map[string]interface{} - -func JsonStr(j Json) string { - data, _ := json.Marshal(j) - return string(data) -} diff --git a/drivers/onedrive.go b/drivers/onedrive.go new file mode 100644 index 00000000..6513def1 --- /dev/null +++ b/drivers/onedrive.go @@ -0,0 +1,307 @@ +package drivers + +import ( + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/go-resty/resty/v2" + "github.com/gofiber/fiber/v2" + "github.com/robfig/cron/v3" + "path/filepath" + "time" +) + +type Onedrive struct{} + +var oneClient = resty.New() + +type OnedriveHost struct { + Oauth string + Api string +} + +var onedriveHostMap = map[string]OnedriveHost{ + "global": { + Oauth: "https://login.microsoftonline.com", + Api: "https://graph.microsoft.com", + }, + "cn": { + Oauth: "https://login.chinacloudapi.cn", + Api: "https://microsoftgraph.chinacloudapi.cn", + }, + "us": { + Oauth: "https://login.microsoftonline.us", + Api: "https://graph.microsoft.us", + }, + "de": { + Oauth: "https://login.microsoftonline.de", + Api: "https://graph.microsoft.de", + }, +} + +func init() { + RegisterDriver("Onedrive", &Onedrive{}) + oneClient.SetRetryCount(3) +} + +func (o Onedrive) GetMetaUrl(account *model.Account, auth bool, path string) string { + path = filepath.Join(account.RootFolder, path) + host, _ := onedriveHostMap[account.Zone] + if auth { + return host.Oauth + } + switch account.OnedriveType { + case "onedrive": + { + if path == "/" { + return fmt.Sprintf("%s/v1.0/me/drive/root", host.Api) + } else { + return fmt.Sprintf("%s/v1.0/me/drive/root:%s:", host.Api, path) + } + } + case "sharepoint": + { + if path == "/" { + return fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, account.SiteId) + } else { + return fmt.Sprintf("%s/v1.0/sites/%s/drive/root:%s:", host.Api, account.SiteId, path) + } + } + default: + return "" + } +} + +func (o Onedrive) Items() []Item { + return []Item{ + { + Name: "zone", + Label: "zone", + Type: "select", + Required: true, + Values: "global,cn,us,de", + Description: "", + }, + { + Name: "onedrive_type", + Label: "onedrive type", + Type: "select", + Required: true, + Values: "onedrive,sharepoint", + }, + { + Name: "client_id", + Label: "client id", + Type: "string", + Required: true, + }, + { + Name: "client_secret", + Label: "client secret", + Type: "string", + Required: true, + }, + { + Name: "redirect_uri", + Label: "redirect uri", + Type: "string", + Required: true, + }, + { + Name: "refresh_token", + Label: "refresh token", + Type: "string", + Required: true, + }, + { + Name: "site_url", + Label: "site url", + Type: "string", + Required: false, + }, + { + Name: "root_folder", + Label: "root folder path", + Type: "string", + Required: false, + }, + } +} + +type OneTokenErr struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +func (o Onedrive) RefreshToken(account *model.Account) error { + url := o.GetMetaUrl(account, true, "") + "/common/oauth2/v2.0/token" + var resp TokenResp + var e OneTokenErr + _, err := oneClient.R().SetResult(&resp).SetError(&e).SetFormData(map[string]string{ + "grant_type": "refresh_token", + "client_id": account.ClientId, + "client_secret": account.ClientSecret, + "redirect_uri": account.RedirectUri, + "refresh_token": account.RefreshToken, + }).Post(url) + if err != nil { + return err + } + if e.Error != "" { + return fmt.Errorf("%s", e.ErrorDescription) + } + account.RefreshToken, account.AccessToken = resp.RefreshToken, resp.AccessToken + return nil +} + +type OneFile struct { + Name string `json:"name"` + Size int64 `json:"size"` + LastModifiedDateTime *time.Time `json:"lastModifiedDateTime"` + Url string `json:"@microsoft.graph.downloadUrl"` + File struct { + MimeType string `json:"mimeType"` + } `json:"file"` +} + +type OneFiles struct { + Value []OneFile `json:"value"` +} + +type OneRespErr struct { + Error struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` +} + +func (o Onedrive) FormatFile(file *OneFile) *model.File { + f := &model.File{ + Name: file.Name, + Size: file.Size, + UpdatedAt: file.LastModifiedDateTime, + Driver: "OneDrive", + Url: file.Url, + } + if file.File.MimeType == "" { + f.Type = conf.FOLDER + } else { + f.Type = utils.GetFileType(filepath.Ext(file.Name)) + } + return f +} + +func (o Onedrive) GetFiles(account *model.Account, path string) ([]OneFile, error) { + var files OneFiles + var e OneRespErr + _, err := oneClient.R().SetResult(&files).SetError(&e). + SetHeader("Authorization", "Bearer "+account.AccessToken). + Get(o.GetMetaUrl(account, false, path) + "/children") + if err != nil { + return nil, err + } + if e.Error.Code != "" { + return nil, fmt.Errorf("%s", e.Error.Message) + } + return files.Value, nil +} + +func (o Onedrive) GetFile(account *model.Account, path string) (*OneFile, error) { + var file OneFile + var e OneRespErr + _, err := oneClient.R().SetResult(&file).SetError(&e). + SetHeader("Authorization", "Bearer "+account.AccessToken). + Get(o.GetMetaUrl(account, false, path)) + if err != nil { + return nil, err + } + if e.Error.Code != "" { + return nil, fmt.Errorf("%s", e.Error.Message) + } + return &file, nil +} + +func (o Onedrive) Path(path string, account *model.Account) (*model.File, []*model.File, error) { + path = utils.ParsePath(path) + cache, err := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path)) + if err == nil { + files, _ := cache.([]*model.File) + return nil, files, nil + } + file, err := o.GetFile(account, path) + if err != nil { + return nil, nil, err + } + if file.File.MimeType != "" { + return o.FormatFile(file), nil, nil + } else { + files, err := o.GetFiles(account, path) + if err != nil { + return nil, nil, err + } + res := make([]*model.File, 0) + for _, file := range files { + res = append(res, o.FormatFile(&file)) + } + _ = conf.Cache.Set(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path), res, nil) + return nil, res, nil + } +} + +func (o Onedrive) Link(path string, account *model.Account) (string, error) { + file, err := o.GetFile(account, path) + if err != nil { + return "", err + } + if file.File.MimeType == "" { + return "", fmt.Errorf("can't down folder") + } + return file.Url, nil +} + +func (o Onedrive) Save(account *model.Account, old *model.Account) error { + _, ok := onedriveHostMap[account.Zone] + if !ok { + return fmt.Errorf("no [%s] zone", account.Zone) + } + if old != nil { + conf.Cron.Remove(cron.EntryID(old.CronId)) + } + account.RootFolder = utils.ParsePath(account.RootFolder) + err := o.RefreshToken(account) + if err != nil { + return err + } + cronId, err := conf.Cron.AddFunc("@every 1h", func() { + name := account.Name + newAccount, ok := model.GetAccount(name) + if !ok { + return + } + err = o.RefreshToken(&newAccount) + if err != nil { + newAccount.Status = err.Error() + } + _ = model.SaveAccount(newAccount) + }) + if err != nil { + return err + } + account.CronId = int(cronId) + err = model.SaveAccount(*account) + if err != nil { + return err + } + return nil +} + +func (o Onedrive) Proxy(ctx *fiber.Ctx) { + +} + +func (o Onedrive) Preview(path string, account *model.Account) (interface{}, error) { + return nil, nil +} + +var _ Driver = (*Onedrive)(nil) diff --git a/model/account.go b/model/account.go index 8132ec00..41edf52e 100644 --- a/model/account.go +++ b/model/account.go @@ -22,6 +22,13 @@ type Account struct { Proxy bool `json:"proxy"` UpdatedAt *time.Time `json:"updated_at"` Search bool `json:"search"` + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + Zone string `json:"zone"` + RedirectUri string `json:"redirect_uri"` + SiteUrl string `json:"site_url"` + SiteId string + OnedriveType string `json:"onedrive_type"` } var accountsMap = map[string]Account{}