diff --git a/agent/constant/backup.go b/agent/constant/backup.go index a2d884148..751316855 100644 --- a/agent/constant/backup.go +++ b/agent/constant/backup.go @@ -15,6 +15,7 @@ const ( Local = "LOCAL" UPYUN = "UPYUN" ALIYUN = "ALIYUN" + GoogleDrive = "GoogleDrive" OneDriveRedirectURI = "http://localhost/login/authorized" ) diff --git a/agent/go.mod b/agent/go.mod index b05a31d89..253af6fb9 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -50,12 +50,14 @@ require ( golang.org/x/oauth2 v0.21.0 golang.org/x/sys v0.22.0 golang.org/x/text v0.16.0 + google.golang.org/api v0.172.0 gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.25.11 ) require ( + cloud.google.com/go/compute/metadata v0.3.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -123,7 +125,10 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/h2non/filetype v1.1.1 // indirect diff --git a/agent/go.sum b/agent/go.sum index edf91b53b..65400e032 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -19,6 +19,8 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -381,14 +383,20 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= @@ -1088,6 +1096,8 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk= +google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/agent/utils/cloud_storage/client/google_drive.go b/agent/utils/cloud_storage/client/google_drive.go new file mode 100644 index 000000000..4450cf6eb --- /dev/null +++ b/agent/utils/cloud_storage/client/google_drive.go @@ -0,0 +1,409 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "strconv" + "strings" + + "github.com/go-resty/resty/v2" +) + +type googleDriveClient struct { + accessToken string +} + +func NewGoogleDriveClient(vars map[string]interface{}) (*googleDriveClient, error) { + accessToken, err := RefreshGoogleToken("refresh_token", "accessToken", vars) + if err != nil { + return nil, err + } + return &googleDriveClient{accessToken: accessToken}, nil +} + +func (g *googleDriveClient) ListBuckets() ([]interface{}, error) { + return nil, nil +} + +func (g *googleDriveClient) Exist(pathItem string) (bool, error) { + pathItem = path.Join("root", pathItem) + if _, err := g.loadFileWithName(pathItem); err != nil { + return false, err + } + return true, nil +} + +func (g *googleDriveClient) Size(pathItem string) (int64, error) { + pathItem = path.Join("root", pathItem) + fileInfo, err := g.loadFileWithName(pathItem) + if err != nil { + return 0, err + } + size, _ := strconv.ParseInt(fileInfo.Size, 10, 64) + return size, nil +} + +func (g *googleDriveClient) Delete(pathItem string) (bool, error) { + pathItem = path.Join("root", pathItem) + fileInfo, err := g.loadFileWithName(pathItem) + if err != nil { + return false, err + } + if len(fileInfo.ID) == 0 { + return false, fmt.Errorf("no such file %s", pathItem) + } + res, err := g.googleRequest("https://www.googleapis.com/drive/v3/files/"+fileInfo.ID, http.MethodDelete, nil, nil) + if err != nil { + return false, err + } + fmt.Println(string(res)) + return true, nil +} + +func (g *googleDriveClient) Upload(src, target string) (bool, error) { + target = path.Join("/root", target) + parentID := "root" + var err error + if path.Dir(target) != "/root" { + parentID, err = g.mkdirWithPath(path.Dir(target)) + if err != nil { + return false, err + } + } + file, err := os.Open(src) + if err != nil { + return false, err + } + defer file.Close() + fileInfo, err := file.Stat() + if err != nil { + return false, err + } + + data := map[string]interface{}{ + "name": fileInfo.Name(), + "parents": []string{parentID}, + } + urlItem := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true" + client := resty.New() + client.SetProxy("http://127.0.0.1:7890") + resp, err := client.R(). + SetHeader("Authorization", "Bearer "+g.accessToken). + SetBody(data). + Post(urlItem) + if err != nil { + return false, err + } + uploadUrl := resp.Header().Get("location") + if _, err := g.googleRequest(uploadUrl, http.MethodPut, func(req *resty.Request) { + req.SetHeader("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)).SetBody(file) + }, nil); err != nil { + return false, err + } + return true, nil +} + +func (g *googleDriveClient) Download(src, target string) (bool, error) { + src = path.Join("/root", src) + fileInfo, err := g.loadFileWithName(src) + if err != nil { + return false, err + } + url := fmt.Sprintf("https://www.googleapis.com/drive/v3/files/%s?alt=media&acknowledgeAbuse=true", fileInfo.ID) + if err := g.handleDownload(url, target); err != nil { + return false, err + } + return true, nil +} + +func (g *googleDriveClient) ListObjects(src string) ([]string, error) { + if len(src) == 0 || src == "root" || src == "/root" { + src = "root" + } else { + src = path.Join("/root", src) + } + fileInfos, err := g.loadDirWithPath(src) + if err != nil { + return nil, err + } + var names []string + for _, item := range fileInfos { + names = append(names, item.Name) + } + return names, nil +} + +type googleFileResp struct { + Files []googleFile `json:"files"` +} +type googleFile struct { + ID string `json:"id"` + Name string `json:"name"` + Size string `json:"size"` +} + +func (g *googleDriveClient) mkdirWithPath(target string) (string, error) { + pathItems := strings.Split(target, "/") + var ( + fileInfos []googleFile + err error + ) + parentID := "root" + for i := 0; i < len(pathItems); i++ { + if len(pathItems[i]) == 0 { + continue + } + fileInfos, err = g.loadFileWithParentID(parentID) + if err != nil { + return "", err + } + isEnd := false + if i == len(pathItems)-2 { + isEnd = true + } + exist := false + for _, item := range fileInfos { + if item.Name == pathItems[i+1] { + parentID = item.ID + if isEnd { + return item.ID, nil + } else { + exist = true + } + } + } + if !exist { + parentID, err = g.mkdir(parentID, pathItems[i+1]) + if err != nil { + return parentID, err + } + if isEnd { + return parentID, nil + } + } + } + return "", errors.New("mkdir failed.") +} + +type googleMkdirRes struct { + ID string `json:"id"` +} + +func (g *googleDriveClient) mkdir(parentID, name string) (string, error) { + data := map[string]interface{}{ + "name": name, + "parents": []string{parentID}, + "mimeType": "application/vnd.google-apps.folder", + } + res, err := g.googleRequest("https://www.googleapis.com/drive/v3/files", http.MethodPost, func(req *resty.Request) { + req.SetBody(data) + }, nil) + if err != nil { + return "", err + } + var mkdirResp googleMkdirRes + if err := json.Unmarshal(res, &mkdirResp); err != nil { + return "", err + } + return mkdirResp.ID, nil +} + +func (g *googleDriveClient) handleDownload(urlItem string, target string) error { + req, err := http.NewRequest(http.MethodGet, urlItem, nil) + if err != nil { + return err + } + req.Header.Add("Authorization", "Bearer "+g.accessToken) + proxyURL, _ := url.Parse("http://127.0.0.1:7890") + client := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + }, + } + response, err := client.Do(req) + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return fmt.Errorf("handle download with url failed, code: %v", response.StatusCode) + } + if _, err := os.Stat(path.Dir(target)); err != nil { + _ = os.MkdirAll(path.Dir(target), os.ModePerm) + } + out, err := os.Create(target) + if err != nil { + return err + } + defer out.Close() + if _, err = io.Copy(out, response.Body); err != nil { + return err + } + + return nil +} + +func (g *googleDriveClient) loadFileWithName(pathItem string) (googleFile, error) { + pathItems := strings.Split(pathItem, "/") + var ( + fileInfos []googleFile + err error + ) + parentID := "root" + for i := 0; i < len(pathItems); i++ { + if len(pathItems[i]) == 0 { + continue + } + fileInfos, err = g.loadFileWithParentID(parentID) + if err != nil { + return googleFile{}, err + } + isEnd := false + if i == len(pathItems)-2 { + isEnd = true + } + exist := false + for _, item := range fileInfos { + if item.Name == pathItems[i+1] { + if isEnd { + return item, nil + } else { + parentID = item.ID + exist = true + } + } + } + if !exist { + return googleFile{}, errors.New("no such file or dir") + } + + } + return googleFile{}, errors.New("no such file or dir") +} + +func (g *googleDriveClient) loadFileWithParentID(parentID string) ([]googleFile, error) { + query := map[string]string{ + "fields": "files(id,name,mimeType,size)", + "q": fmt.Sprintf("'%s' in parents and trashed = false", parentID), + } + + res, err := g.googleRequest("https://www.googleapis.com/drive/v3/files", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(query) + }, nil) + if err != nil { + return nil, err + } + var fileResp googleFileResp + if err := json.Unmarshal(res, &fileResp); err != nil { + return nil, err + } + return fileResp.Files, nil +} + +func (g googleDriveClient) loadDirWithPath(path string) ([]googleFile, error) { + pathItems := strings.Split(path, "/") + var ( + fileInfos []googleFile + err error + ) + parentID := "root" + for i := 0; i < len(pathItems); i++ { + if len(pathItems[i]) == 0 { + continue + } + fileInfos, err = g.loadFileWithParentID(parentID) + if err != nil { + return fileInfos, err + } + if i == len(pathItems)-1 { + return fileInfos, nil + } + exist := false + for _, item := range fileInfos { + if item.Name == pathItems[i+1] { + parentID = item.ID + exist = true + } + } + if !exist { + return nil, errors.New("no such file or dir") + } + } + return fileInfos, errors.New("no such file or dir") +} + +type reqCallback func(req *resty.Request) + +func (g *googleDriveClient) googleRequest(urlItem, method string, callback reqCallback, resp interface{}) ([]byte, error) { + client := resty.New() + client.SetProxy("http://127.0.0.1:7890") + req := client.R() + req.SetHeader("Authorization", "Bearer "+g.accessToken) + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(req) + } + res, err := req.Execute(method, urlItem) + if err != nil { + return nil, err + } + + if res.StatusCode() == 401 { + // refresh and retry + return nil, fmt.Errorf("request for %s failed, code: %v", urlItem, res.StatusCode()) + } + if res.StatusCode() > 300 { + return nil, fmt.Errorf("request for %s failed, err: %v", urlItem, res.StatusCode()) + } + return res.Body(), nil +} + +type googleTokenRes struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func RefreshGoogleToken(grantType string, tokenType string, varMap map[string]interface{}) (string, error) { + client := resty.New() + client.SetProxy("http://127.0.0.1:7890") + data := map[string]interface{}{ + "client_id": loadParamFromVars("client_id", varMap), + "client_secret": loadParamFromVars("client_secret", varMap), + "redirect_uri": loadParamFromVars("redirect_uri", varMap), + } + if grantType == "refresh_token" { + data["grant_type"] = "refresh_token" + data["refresh_token"] = loadParamFromVars("refresh_token", varMap) + } else { + data["grant_type"] = "authorization_code" + data["code"] = loadParamFromVars("code", varMap) + } + urlItem := "https://www.googleapis.com/oauth2/v4/token" + resp, err := client.R(). + SetBody(data). + Post(urlItem) + if err != nil { + return "", fmt.Errorf("load account token failed, err: %v", err) + } + + if resp.StatusCode() != 200 { + return "", fmt.Errorf("load account token failed, code: %v", resp.StatusCode()) + } + var respItem googleTokenRes + if err := json.Unmarshal(resp.Body(), &respItem); err != nil { + return "", err + } + fmt.Println(respItem) + if tokenType == "accessToken" { + return respItem.AccessToken, nil + } + return respItem.RefreshToken, nil +} diff --git a/core/app/api/v2/backup.go b/core/app/api/v2/backup.go index 11e286baa..4e2eb3f54 100644 --- a/core/app/api/v2/backup.go +++ b/core/app/api/v2/backup.go @@ -1,6 +1,8 @@ package v2 import ( + "fmt" + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/gin-gonic/gin" @@ -67,9 +69,14 @@ func (b *BaseApi) ListBuckets(c *gin.Context) { // @Accept json // @Success 200 {object} dto.OneDriveInfo // @Security ApiKeyAuth -// @Router /core/backup/onedrive [get] +// @Router /core/backup/client/:clientType [get] func (b *BaseApi) LoadOneDriveInfo(c *gin.Context) { - data, err := backupService.LoadOneDriveInfo() + clientType, ok := c.Params.Get("clientType") + if !ok { + helper.BadRequest(c, fmt.Errorf("error %s in path", "clientType")) + return + } + data, err := backupService.LoadBackupClientInfo(clientType) if err != nil { helper.InternalServer(c, err) return diff --git a/core/app/dto/backup.go b/core/app/dto/backup.go index 638974f8b..8379cb160 100644 --- a/core/app/dto/backup.go +++ b/core/app/dto/backup.go @@ -35,7 +35,7 @@ type BackupInfo struct { RememberAuth bool `json:"rememberAuth"` } -type OneDriveInfo struct { +type BackupClientInfo struct { ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` RedirectUri string `json:"redirect_uri"` diff --git a/core/app/model/backup.go b/core/app/model/backup.go index 58fd1f3bc..704febc4a 100644 --- a/core/app/model/backup.go +++ b/core/app/model/backup.go @@ -12,8 +12,6 @@ type BackupAccount struct { BackupPath string `json:"backupPath"` Vars string `json:"vars"` - RememberAuth bool `json:"rememberAuth"` - EntryID uint `json:"entryID"` - - DeletedAt time.Time `json:"deletedAt"` + RememberAuth bool `json:"rememberAuth"` + DeletedAt time.Time `json:"deletedAt"` } diff --git a/core/app/service/backup.go b/core/app/service/backup.go index ddf6e9c4a..ef95f5a06 100644 --- a/core/app/service/backup.go +++ b/core/app/service/backup.go @@ -24,7 +24,6 @@ import ( "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/jinzhu/copier" "github.com/pkg/errors" - "github.com/robfig/cron/v3" ) type BackupService struct{} @@ -36,7 +35,7 @@ type IBackupService interface { GetLocalDir() (string, error) LoadBackupOptions() ([]dto.BackupOption, error) SearchWithPage(search dto.SearchPageWithType) (int64, interface{}, error) - LoadOneDriveInfo() (dto.OneDriveInfo, error) + LoadBackupClientInfo(clientType string) (dto.BackupClientInfo, error) Create(backupDto dto.BackupOperate) error GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) Update(req dto.BackupOperate) error @@ -156,7 +155,7 @@ func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, inter item.Credential = base64.StdEncoding.EncodeToString([]byte(item.Credential)) } - if account.Type == constant.OneDrive || account.Type == constant.ALIYUN { + if account.Type == constant.OneDrive || account.Type == constant.ALIYUN || account.Type == constant.GoogleDrive { varMap := make(map[string]interface{}) if err := json.Unmarshal([]byte(item.Vars), &varMap); err != nil { continue @@ -171,10 +170,18 @@ func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, inter return count, data, nil } -func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) { - var data dto.OneDriveInfo - data.RedirectUri = constant.OneDriveRedirectURI - clientID, err := settingRepo.Get(commonRepo.WithByKey("OneDriveID")) +func (u *BackupService) LoadBackupClientInfo(clientType string) (dto.BackupClientInfo, error) { + var data dto.BackupClientInfo + clientIDKey := "OneDriveID" + clientIDSc := "OneDriveSc" + if clientType == constant.GoogleDrive { + clientIDKey = "GoogleID" + clientIDSc = "GoogleSc" + data.RedirectUri = constant.GoogleRedirectURI + } else { + data.RedirectUri = constant.OneDriveRedirectURI + } + clientID, err := settingRepo.Get(commonRepo.WithByKey(clientIDKey)) if err != nil { return data, err } @@ -183,7 +190,7 @@ func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) { return data, err } data.ClientID = string(idItem) - clientSecret, err := settingRepo.Get(commonRepo.WithByKey("OneDriveSc")) + clientSecret, err := settingRepo.Get(commonRepo.WithByKey(clientIDSc)) if err != nil { return data, err } @@ -215,8 +222,8 @@ func (u *BackupService) Create(req dto.BackupOperate) error { } backup.Credential = string(itemCredential) - if req.Type == constant.OneDrive { - if err := u.loadAccessToken(&backup); err != nil { + if req.Type == constant.OneDrive || req.Type == constant.GoogleDrive { + if err := u.loadRefreshTokenByCode(&backup); err != nil { return err } } @@ -225,11 +232,6 @@ func (u *BackupService) Create(req dto.BackupOperate) error { return buserr.WithMap("ErrBackupCheck", map[string]interface{}{"err": err.Error()}, err) } } - if backup.Type == constant.OneDrive { - if err := StartRefreshOneDriveToken(&backup); err != nil { - return err - } - } backup.AccessKey, err = encrypt.StringEncrypt(backup.AccessKey) if err != nil { @@ -284,9 +286,6 @@ func (u *BackupService) Delete(id uint) error { if backup.Type == constant.Local { return buserr.New(constant.ErrBackupLocalDelete) } - if backup.Type == constant.OneDrive { - global.Cron.Remove(cron.EntryID(backup.EntryID)) - } if _, err := httpUtils.NewLocalClient(fmt.Sprintf("/api/v2/backups/check/%v", id), http.MethodGet, nil); err != nil { global.LOG.Errorf("check used of local cronjob failed, err: %v", err) return buserr.New(constant.ErrBackupInUsed) @@ -338,9 +337,8 @@ func (u *BackupService) Update(req dto.BackupOperate) error { } } - if newBackup.Type == constant.OneDrive { - global.Cron.Remove(cron.EntryID(backup.EntryID)) - if err := u.loadAccessToken(&backup); err != nil { + if newBackup.Type == constant.OneDrive || newBackup.Type == constant.GoogleDrive { + if err := u.loadRefreshTokenByCode(&backup); err != nil { return err } } @@ -392,14 +390,23 @@ func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.Cl return backClient, nil } -func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error { +func (u *BackupService) loadRefreshTokenByCode(backup *model.BackupAccount) error { varMap := make(map[string]interface{}) if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { return fmt.Errorf("unmarshal backup vars failed, err: %v", err) } - refreshToken, err := client.RefreshToken("authorization_code", "refreshToken", varMap) - if err != nil { - return err + refreshToken := "" + var err error + if backup.Type == constant.GoogleDrive { + refreshToken, err = client.RefreshGoogleToken("authorization_code", "refreshToken", varMap) + if err != nil { + return err + } + } else { + refreshToken, err = client.RefreshToken("authorization_code", "refreshToken", varMap) + if err != nil { + return err + } } delete(varMap, "code") varMap["refresh_status"] = constant.StatusSuccess @@ -489,87 +496,59 @@ func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, erro return client.Upload(fileItem, targetPath) } -func StartRefreshOneDriveToken(backup *model.BackupAccount) error { +func StartRefreshForToken() error { service := NewIBackupService() - oneDriveCronID, err := global.Cron.AddJob("0 3 */31 * *", service) + refreshID, err := global.Cron.AddJob("0 3 */31 * *", service) if err != nil { - global.LOG.Errorf("can not add OneDrive corn job: %s", err.Error()) + global.LOG.Errorf("add cron job of refresh backup account token failed, err: %s", err.Error()) return err } - backup.EntryID = uint(oneDriveCronID) + global.BackupAccountTokenEntryID = refreshID return nil } func (u *BackupService) Run() { - refreshOneDrive() - refreshALIYUN() + refreshToken() } -func refreshOneDrive() { +func refreshToken() { var backups []model.BackupAccount - _ = global.DB.Where("`type` = ?", "OneDrive").Find(&backups) + _ = global.DB.Where("`type` in (?)", []string{constant.OneDrive, constant.ALIYUN, constant.GoogleDrive}).Find(&backups) + if len(backups) == 0 { + return + } for _, backupItem := range backups { if backupItem.ID == 0 { - return + continue } - global.LOG.Infof("start to refresh token of OneDrive %s ...", backupItem.Name) varMap := make(map[string]interface{}) if err := json.Unmarshal([]byte(backupItem.Vars), &varMap); err != nil { - global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) - return + global.LOG.Errorf("Failed to refresh %s - %s token, please retry, err: %v", backupItem.Type, backupItem.Name, err) + continue + } + var ( + refreshToken string + err error + ) + switch backupItem.Type { + case constant.OneDrive: + refreshToken, err = client.RefreshToken("refresh_token", "refreshToken", varMap) + case constant.GoogleDrive: + refreshToken, err = client.RefreshGoogleToken("refresh_token", "refreshToken", varMap) + case constant.ALIYUN: + refreshToken, err = client.RefreshALIToken(varMap) } - refreshToken, err := client.RefreshToken("refresh_token", "refreshToken", varMap) - varMap["refresh_status"] = constant.StatusSuccess - varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) if err != nil { varMap["refresh_status"] = constant.StatusFailed varMap["refresh_msg"] = err.Error() global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) - return + continue } - varMap["refresh_token"] = refreshToken - - varsItem, _ := json.Marshal(varMap) - _ = global.DB.Model(&model.BackupAccount{}). - Where("id = ?", backupItem.ID). - Updates(map[string]interface{}{ - "vars": varsItem, - }).Error - global.LOG.Info("Successfully refreshed OneDrive token.") - } -} - -func refreshALIYUN() { - var backups []model.BackupAccount - _ = global.DB.Where("`type` = ?", "ALIYUN").Find(&backups) - for _, backupItem := range backups { - global.LOG.Infof("start to refresh token of ALIYUN %s ...", backupItem.Name) - varMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(backupItem.Vars), &varMap); err != nil { - global.LOG.Errorf("Failed to refresh ALIYUN token, please retry, err: %v", err) - return - } - if _, ok := varMap["refresh_token"]; !ok { - global.LOG.Error("no such refresh token find in db") - return - } - refreshToken, err := client.RefreshALIToken(fmt.Sprintf("%v", varMap["refresh_token"])) varMap["refresh_status"] = constant.StatusSuccess varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) - if err != nil { - varMap["refresh_status"] = constant.StatusFailed - varMap["refresh_msg"] = err.Error() - global.LOG.Errorf("Failed to refresh ALIYUN token, please retry, err: %v", err) - return - } varMap["refresh_token"] = refreshToken varsItem, _ := json.Marshal(varMap) - _ = global.DB.Model(&model.BackupAccount{}). - Where("id = ?", backupItem.ID). - Updates(map[string]interface{}{ - "vars": varsItem, - }).Error - global.LOG.Info("Successfully refreshed ALIYUN token.") + _ = global.DB.Model(&model.BackupAccount{}).Where("id = ?", backupItem.ID).Updates(map[string]interface{}{"vars": varsItem}).Error } } diff --git a/core/constant/common.go b/core/constant/common.go index b165927bc..0345e7e75 100644 --- a/core/constant/common.go +++ b/core/constant/common.go @@ -19,16 +19,19 @@ const ( StatusDisable = "Disable" // backup - S3 = "S3" - OSS = "OSS" - Sftp = "SFTP" - OneDrive = "OneDrive" - MinIo = "MINIO" - Cos = "COS" - Kodo = "KODO" - WebDAV = "WebDAV" - Local = "LOCAL" - UPYUN = "UPYUN" - ALIYUN = "ALIYUN" + S3 = "S3" + OSS = "OSS" + Sftp = "SFTP" + OneDrive = "OneDrive" + MinIo = "MINIO" + Cos = "COS" + Kodo = "KODO" + WebDAV = "WebDAV" + Local = "LOCAL" + UPYUN = "UPYUN" + ALIYUN = "ALIYUN" + GoogleDrive = "GoogleDrive" + OneDriveRedirectURI = "http://localhost/login/authorized" + GoogleRedirectURI = "http://localhost:8080" ) diff --git a/core/global/global.go b/core/global/global.go index e27b7946f..36765352d 100644 --- a/core/global/global.go +++ b/core/global/global.go @@ -26,4 +26,6 @@ var ( I18n *i18n.Localizer Cron *cron.Cron + + BackupAccountTokenEntryID cron.EntryID ) diff --git a/core/init/cron/cron.go b/core/init/cron/cron.go index db215b3c3..33e12d03a 100644 --- a/core/init/cron/cron.go +++ b/core/init/cron/cron.go @@ -3,7 +3,6 @@ package cron import ( "time" - "github.com/1Panel-dev/1Panel/core/app/model" "github.com/1Panel-dev/1Panel/core/app/service" "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/utils/common" @@ -14,10 +13,6 @@ func Init() { nyc, _ := time.LoadLocation(common.LoadTimeZone()) global.Cron = cron.New(cron.WithLocation(nyc), cron.WithChain(cron.Recover(cron.DefaultLogger)), cron.WithChain(cron.DelayIfStillRunning(cron.DefaultLogger))) - var accounts []model.BackupAccount - _ = global.DB.Where("type = ?", "OneDrive").Find(&accounts).Error - for i := 0; i < len(accounts); i++ { - _ = service.StartRefreshOneDriveToken(&accounts[i]) - } + _ = service.StartRefreshForToken() global.Cron.Start() } diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 1e8f36de2..060fbd27b 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -17,6 +17,7 @@ func Init() { migrations.InitTerminalSetting, migrations.InitAppLauncher, migrations.InitBackup, + migrations.InitGoogle, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index ab0a42c24..8a8288f02 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -247,3 +247,16 @@ var InitBackup = &gormigrate.Migration{ return tx.AutoMigrate(&model.BackupAccount{}) }, } + +var InitGoogle = &gormigrate.Migration{ + ID: "20241111-init-google", + Migrate: func(tx *gorm.DB) error { + if err := tx.Create(&model.Setting{Key: "GoogleID", Value: "NTU2NTQ3NDYwMTQtY2Q0bGR0dDk2aGNsNWcxYWtwdmJhZTFmcjJlZ2Y0MXAuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20K"}).Error; err != nil { + return err + } + if err := tx.Create(&model.Setting{Key: "GoogleSc", Value: "R09DU1BYLXRibXg0QVdVZ3d3Ykc2QW1XTHQ3YUdaZElVeE4K"}).Error; err != nil { + return err + } + return nil + }, +} diff --git a/core/utils/cloud_storage/client/ali.go b/core/utils/cloud_storage/client/ali.go index b6d62e812..6a5084211 100644 --- a/core/utils/cloud_storage/client/ali.go +++ b/core/utils/cloud_storage/client/ali.go @@ -19,10 +19,9 @@ type aliClient struct { } func NewALIClient(vars map[string]interface{}) (*aliClient, error) { - refresh_token := loadParamFromVars("refresh_token", vars) drive_id := loadParamFromVars("drive_id", vars) - token, err := RefreshALIToken(refresh_token) + token, err := RefreshALIToken(vars) if err != nil { return nil, err } @@ -278,7 +277,11 @@ type tokenResp struct { AccessToken string `json:"access_token"` } -func RefreshALIToken(refresh_token string) (string, error) { +func RefreshALIToken(varMap map[string]interface{}) (string, error) { + refresh_token := loadParamFromVars("refresh_token", varMap) + if len(refresh_token) == 0 { + return "", errors.New("no such refresh token find in db") + } client := resty.New() data := map[string]interface{}{ "grant_type": "refresh_token", diff --git a/core/utils/cloud_storage/client/google_drive.go b/core/utils/cloud_storage/client/google_drive.go new file mode 100644 index 000000000..e38ce7762 --- /dev/null +++ b/core/utils/cloud_storage/client/google_drive.go @@ -0,0 +1,231 @@ +package client + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "path" + "strconv" + "strings" + + "github.com/go-resty/resty/v2" +) + +type googleDriveClient struct { + accessToken string +} + +func NewGoogleDriveClient(vars map[string]interface{}) (*googleDriveClient, error) { + accessToken, err := RefreshGoogleToken("refresh_token", "accessToken", vars) + if err != nil { + return nil, err + } + return &googleDriveClient{accessToken: accessToken}, nil +} + +func (g *googleDriveClient) ListBuckets() ([]interface{}, error) { + return nil, nil +} + +func (g *googleDriveClient) Upload(src, target string) (bool, error) { + target = path.Join("/root", target) + parentID := "root" + var err error + if path.Dir(target) != "/root" { + parentID, err = g.mkdirWithPath(path.Dir(target)) + if err != nil { + return false, err + } + } + file, err := os.Open(src) + if err != nil { + return false, err + } + defer file.Close() + fileInfo, err := file.Stat() + if err != nil { + return false, err + } + + data := map[string]interface{}{ + "name": fileInfo.Name(), + "parents": []string{parentID}, + } + urlItem := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true" + client := resty.New() + client.SetProxy("http://127.0.0.1:7890") + resp, err := client.R(). + SetHeader("Authorization", "Bearer "+g.accessToken). + SetBody(data). + Post(urlItem) + if err != nil { + return false, err + } + uploadUrl := resp.Header().Get("location") + if _, err := g.googleRequest(uploadUrl, http.MethodPut, func(req *resty.Request) { + req.SetHeader("Content-Length", strconv.FormatInt(fileInfo.Size(), 10)).SetBody(file) + }, nil); err != nil { + return false, err + } + return true, nil +} + +type googleFileResp struct { + Files []googleFile `json:"files"` +} +type googleFile struct { + ID string `json:"id"` + Name string `json:"name"` + Size string `json:"size"` +} + +func (g *googleDriveClient) mkdirWithPath(target string) (string, error) { + pathItems := strings.Split(target, "/") + var ( + fileInfos []googleFile + err error + ) + parentID := "root" + for i := 0; i < len(pathItems); i++ { + if len(pathItems[i]) == 0 { + continue + } + fileInfos, err = g.loadFileWithParentID(parentID) + if err != nil { + return "", err + } + isEnd := false + if i == len(pathItems)-2 { + isEnd = true + } + exist := false + for _, item := range fileInfos { + if item.Name == pathItems[i+1] { + parentID = item.ID + if isEnd { + return item.ID, nil + } else { + exist = true + } + } + } + if !exist { + parentID, err = g.mkdir(parentID, pathItems[i+1]) + if err != nil { + return parentID, err + } + if isEnd { + return parentID, nil + } + } + } + return "", errors.New("mkdir failed.") +} + +type googleMkdirRes struct { + ID string `json:"id"` +} + +func (g *googleDriveClient) mkdir(parentID, name string) (string, error) { + data := map[string]interface{}{ + "name": name, + "parents": []string{parentID}, + "mimeType": "application/vnd.google-apps.folder", + } + res, err := g.googleRequest("https://www.googleapis.com/drive/v3/files", http.MethodPost, func(req *resty.Request) { + req.SetBody(data) + }, nil) + if err != nil { + return "", err + } + var mkdirResp googleMkdirRes + if err := json.Unmarshal(res, &mkdirResp); err != nil { + return "", err + } + return mkdirResp.ID, nil +} + +func (g *googleDriveClient) loadFileWithParentID(parentID string) ([]googleFile, error) { + query := map[string]string{ + "fields": "files(id,name,mimeType,size)", + "q": fmt.Sprintf("'%s' in parents and trashed = false", parentID), + } + + res, err := g.googleRequest("https://www.googleapis.com/drive/v3/files", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(query) + }, nil) + if err != nil { + return nil, err + } + var fileResp googleFileResp + if err := json.Unmarshal(res, &fileResp); err != nil { + return nil, err + } + return fileResp.Files, nil +} + +type reqCallback func(req *resty.Request) + +func (g *googleDriveClient) googleRequest(urlItem, method string, callback reqCallback, resp interface{}) ([]byte, error) { + client := resty.New() + client.SetProxy("http://127.0.0.1:7890") + req := client.R() + req.SetHeader("Authorization", "Bearer "+g.accessToken) + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(req) + } + res, err := req.Execute(method, urlItem) + if err != nil { + return nil, err + } + if res.StatusCode() > 300 { + return nil, fmt.Errorf("request for %s failed, err: %v", urlItem, res.StatusCode()) + } + return res.Body(), nil +} + +type googleTokenRes struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func RefreshGoogleToken(grantType string, tokenType string, varMap map[string]interface{}) (string, error) { + client := resty.New() + client.SetProxy("http://127.0.0.1:7890") + data := map[string]interface{}{ + "client_id": loadParamFromVars("client_id", varMap), + "client_secret": loadParamFromVars("client_secret", varMap), + "redirect_uri": loadParamFromVars("redirect_uri", varMap), + } + if grantType == "refresh_token" { + data["grant_type"] = "refresh_token" + data["refresh_token"] = loadParamFromVars("refresh_token", varMap) + } else { + data["grant_type"] = "authorization_code" + data["code"] = loadParamFromVars("code", varMap) + } + urlItem := "https://www.googleapis.com/oauth2/v4/token" + resp, err := client.R(). + SetBody(data). + Post(urlItem) + if err != nil { + return "", fmt.Errorf("load account token failed, err: %v", err) + } + + if resp.StatusCode() != 200 { + return "", fmt.Errorf("load account token failed, code: %v", resp.StatusCode()) + } + var respItem googleTokenRes + if err := json.Unmarshal(resp.Body(), &respItem); err != nil { + return "", err + } + if tokenType == "accessToken" { + return respItem.AccessToken, nil + } + return respItem.RefreshToken, nil +} diff --git a/core/utils/cloud_storage/cloud_storage_client.go b/core/utils/cloud_storage/cloud_storage_client.go index 8ca5ce041..0ffd2061f 100644 --- a/core/utils/cloud_storage/cloud_storage_client.go +++ b/core/utils/cloud_storage/cloud_storage_client.go @@ -34,6 +34,8 @@ func NewCloudStorageClient(backupType string, vars map[string]interface{}) (Clou return client.NewUpClient(vars) case constant.ALIYUN: return client.NewALIClient(vars) + case constant.GoogleDrive: + return client.NewGoogleDriveClient(vars) default: return nil, constant.ErrNotSupportType } diff --git a/frontend/src/api/modules/backup.ts b/frontend/src/api/modules/backup.ts index 512b77ad6..57605820a 100644 --- a/frontend/src/api/modules/backup.ts +++ b/frontend/src/api/modules/backup.ts @@ -44,8 +44,8 @@ export const getLocalBackupDir = () => { export const searchBackup = (params: Backup.SearchWithType) => { return http.post>(`/core/backup/search`, params); }; -export const getOneDriveInfo = () => { - return http.get(`/core/backup/onedrive`); +export const getClientInfo = (clientType: string) => { + return http.get(`/core/backup/client/${clientType}`); }; export const addBackup = (params: Backup.BackupOperate) => { let request = deepCopy(params) as Backup.BackupOperate; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index a376260dd..b9b01e58d 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1457,6 +1457,7 @@ const message = { 'The current maximum limit for non-client downloads on Aliyun Drive is 100 MB. Exceeding this limit requires downloading through the client.', ALIYUNRecover: 'The current maximum limit for non-client downloads on Aliyun Drive is 100 MB. Exceeding this limit requires downloading through the client to the local device, then synchronizing the snapshot for recovery.', + GoogleDrive: 'Google Drive', analysis: 'Analysis', analysisHelper: 'Paste the entire token content to automatically parse the required parts. For specific operations, please refer to the official documentation.', @@ -1476,7 +1477,7 @@ const message = { codeWarning: 'The current authorization code format is incorrect, please confirm again!', code: 'Auth code', codeHelper: - 'Please click on the "Acquire" button, then login to OneDrive and copy the content after "code" in the redirected link. Paste it into this input box. For specific instructions, please refer to the official documentation.', + 'Please click on the "Acquire" button, then login to {0} and copy the content after "code" in the redirected link. Paste it into this input box. For specific instructions, please refer to the official documentation.', loadCode: 'Acquire', COS: 'Tencent COS', ap_beijing_1: 'Beijing Zone 1', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 559426110..412fba75a 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -1369,6 +1369,7 @@ const message = { ALIYUNHelper: '當前阿里雲盤非客戶端下載最大限制為 100 MB,超過限制需要通過客戶端下載', ALIYUNRecover: '當前阿里雲盤非客戶端下載最大限制為 100 MB,超過限制需要通過客戶端下載到本地後,同步快照進行恢復', + GoogleDrive: '谷歌云盘', analysis: '解析', analysisHelper: '粘貼整個 token 內容,自動解析所需部分,具體操作可參考官方文檔', serviceName: '服務名稱', @@ -1387,7 +1388,7 @@ const message = { backupDir: '備份目录', code: '授權碼', codeHelper: - '請點擊獲取按鈕,然後登錄 OneDrive 復製跳轉鏈接中 code 後面的內容,粘貼到該輸入框中,具體操作可參考官方文檔。', + '請點擊獲取按鈕,然後登錄 {0} 復製跳轉鏈接中 code 後面的內容,粘貼到該輸入框中,具體操作可參考官方文檔。', loadCode: '獲取', COS: '騰訊雲 COS', ap_beijing_1: '北京一區', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index f7c35eb47..11c51f06e 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1371,6 +1371,7 @@ const message = { ALIYUNHelper: '当前阿里云盘非客户端下载最大限制为 100 MB,超过限制需要通过客户端下载', ALIYUNRecover: '当前阿里云盘非客户端下载最大限制为 100 MB,超过限制需要通过客户端下载到本地后,同步快照进行恢复', + GoogleDrive: '谷歌云盘', analysis: '解析', analysisHelper: '粘贴整个 token 内容,自动解析所需部分,具体操作可参考官方文档', serviceName: '服务名称', @@ -1389,7 +1390,7 @@ const message = { backupDir: '备份目录', code: '授权码', codeHelper: - '请点击获取按钮,然后登录 OneDrive 复制跳转链接中 code 后面的内容,粘贴到该输入框中,具体操作可参考官方文档。', + '请点击获取按钮,然后登录 {0} 复制跳转链接中 code 后面的内容,粘贴到该输入框中,具体操作可参考官方文档。', loadCode: '获取', COS: '腾讯云 COS', ap_beijing_1: '北京一区', diff --git a/frontend/src/views/setting/backup-account/operate/index.vue b/frontend/src/views/setting/backup-account/operate/index.vue index 60cca0fb9..9d4c44348 100644 --- a/frontend/src/views/setting/backup-account/operate/index.vue +++ b/frontend/src/views/setting/backup-account/operate/index.vue @@ -18,6 +18,7 @@ + {{ $t('setting.ALIYUNHelper') }} @@ -242,7 +243,7 @@ clearable v-model.trim="dialogData.rowData!.varsJson['token']" /> - + {{ $t('setting.analysis') }} @@ -266,9 +267,9 @@ -
- - +
+ + {{ $t('setting.isNotCN') }} {{ $t('setting.isCN') }} @@ -310,12 +311,12 @@ clearable v-model.trim="dialogData.rowData!.varsJson['code']" /> - + {{ $t('setting.loadCode') }}
- {{ $t('setting.codeHelper') }} + {{ $t('setting.codeHelper', [$t('setting.' + dialogData.rowData?.type)]) }} { const toWebDAVDoc = () => { window.open('https://1panel.cn/docs/user_manual/settings/#webdav-alist', '_blank', 'noopener,noreferrer'); }; -const jumpAzure = async (formEl: FormInstance | undefined) => { +const jumpForCode = async (formEl: FormInstance | undefined) => { if (!formEl) return; const result = await formEl.validateField('varsJson.client_id', callback); if (!result) { @@ -455,12 +456,18 @@ const jumpAzure = async (formEl: FormInstance | undefined) => { } let client_id = dialogData.value.rowData.varsJson['client_id']; let redirect_uri = dialogData.value.rowData.varsJson['redirect_uri']; - let commonUrl = `response_type=code&client_id=${client_id}&redirect_uri=${redirect_uri}&scope=offline_access+Files.ReadWrite.All+User.Read`; - if (!dialogData.value.rowData!.varsJson['isCN']) { - window.open('https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + commonUrl, '_blank'); - } else { - window.open('https://login.chinacloudapi.cn/common/oauth2/v2.0/authorize?' + commonUrl, '_blank'); + if (isOneDrive()) { + let commonUrl = `response_type=code&client_id=${client_id}&redirect_uri=${redirect_uri}&scope=offline_access+Files.ReadWrite.All+User.Read`; + if (!dialogData.value.rowData!.varsJson['isCN']) { + window.open('https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + commonUrl, '_blank'); + } else { + window.open('https://login.chinacloudapi.cn/common/oauth2/v2.0/authorize?' + commonUrl, '_blank'); + } + return; } + + let url = `https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?client_id=${client_id}&response_type=code&redirect_uri=${redirect_uri}&scope=openid%20profile%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fphotoslibrary&access_type=offline&prompt=consent&service=lso&o2v=1&ddm=1&flowName=GeneralOAuthFlow`; + window.open(url, '_blank'); }; function callback(error: any) { if (error) { @@ -470,7 +477,7 @@ function callback(error: any) { } } -const loadFromToken = () => { +const loadFromTokenForAliyun = () => { const obj = JSON.parse(dialogData.value.rowData!.varsJson['token']); dialogData.value.rowData!.varsJson['drive_id'] = obj.default_drive_id; dialogData.value.rowData!.varsJson['refresh_token'] = obj.refresh_token; @@ -479,9 +486,18 @@ const hasRemember = () => { return ( dialogData.value.rowData!.type !== 'LOCAL' && dialogData.value.rowData!.type !== 'OneDrive' && - dialogData.value.rowData!.type !== 'ALIYUN' + dialogData.value.rowData!.type !== 'ALIYUN' && + dialogData.value.rowData!.type !== 'GoogleDrive' ); }; +const hasClient = () => { + let itemType = dialogData.value.rowData!.type; + return itemType === 'OneDrive' || itemType === 'GoogleDrive'; +}; +const isOneDrive = () => { + let itemType = dialogData.value.rowData!.type; + return itemType === 'OneDrive'; +}; const isUPYUN = () => { let itemType = dialogData.value.rowData!.type; return itemType === 'UPYUN'; @@ -522,7 +538,7 @@ const changeType = async () => { break; case 'OneDrive': dialogData.value.rowData.varsJson['isCN'] = false; - const res = await getOneDriveInfo(); + const res = await getClientInfo('Onedrive'); oneDriveInfo.value = res.data; if (!dialogData.value.rowData.id) { dialogData.value.rowData.varsJson = { @@ -532,12 +548,22 @@ const changeType = async () => { redirect_uri: res.data.redirect_uri, }; } + case 'GoogleDrive': + const res2 = await getClientInfo('GoogleDrive'); + oneDriveInfo.value = res2.data; + if (!dialogData.value.rowData.id) { + dialogData.value.rowData.varsJson = { + client_id: res2.data.client_id, + client_secret: res2.data.client_secret, + redirect_uri: res2.data.redirect_uri, + }; + } case 'SFTP': dialogData.value.rowData.varsJson['port'] = 22; dialogData.value.rowData.varsJson['authMode'] = 'password'; } }; -const changeFrom = () => { +const changeOnedriveFrom = () => { if (dialogData.value.rowData.varsJson['isCN']) { dialogData.value.rowData.varsJson = { isCN: true,