1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: 备份账号增加 Google Drive (#7004)

This commit is contained in:
ssongliu 2024-11-12 13:35:12 +08:00 committed by GitHub
parent 9e82e2837d
commit 03e9077c9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 818 additions and 130 deletions

View File

@ -15,6 +15,7 @@ const (
Local = "LOCAL" Local = "LOCAL"
UPYUN = "UPYUN" UPYUN = "UPYUN"
ALIYUN = "ALIYUN" ALIYUN = "ALIYUN"
GoogleDrive = "GoogleDrive"
OneDriveRedirectURI = "http://localhost/login/authorized" OneDriveRedirectURI = "http://localhost/login/authorized"
) )

View File

@ -50,12 +50,14 @@ require (
golang.org/x/oauth2 v0.21.0 golang.org/x/oauth2 v0.21.0
golang.org/x/sys v0.22.0 golang.org/x/sys v0.22.0
golang.org/x/text v0.16.0 golang.org/x/text v0.16.0
google.golang.org/api v0.172.0
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.11 gorm.io/gorm v1.25.11
) )
require ( require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // 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-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.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/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/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/h2non/filetype v1.1.1 // indirect github.com/h2non/filetype v1.1.1 // indirect

View File

@ -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.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.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/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.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/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 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 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 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/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 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 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.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.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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.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.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.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 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 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.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.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 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.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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

View File

@ -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
}

View File

@ -1,6 +1,8 @@
package v2 package v2
import ( import (
"fmt"
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/dto"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -67,9 +69,14 @@ func (b *BaseApi) ListBuckets(c *gin.Context) {
// @Accept json // @Accept json
// @Success 200 {object} dto.OneDriveInfo // @Success 200 {object} dto.OneDriveInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /core/backup/onedrive [get] // @Router /core/backup/client/:clientType [get]
func (b *BaseApi) LoadOneDriveInfo(c *gin.Context) { 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 { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return

View File

@ -35,7 +35,7 @@ type BackupInfo struct {
RememberAuth bool `json:"rememberAuth"` RememberAuth bool `json:"rememberAuth"`
} }
type OneDriveInfo struct { type BackupClientInfo struct {
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
RedirectUri string `json:"redirect_uri"` RedirectUri string `json:"redirect_uri"`

View File

@ -13,7 +13,5 @@ type BackupAccount struct {
Vars string `json:"vars"` Vars string `json:"vars"`
RememberAuth bool `json:"rememberAuth"` RememberAuth bool `json:"rememberAuth"`
EntryID uint `json:"entryID"`
DeletedAt time.Time `json:"deletedAt"` DeletedAt time.Time `json:"deletedAt"`
} }

View File

@ -24,7 +24,6 @@ import (
"github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/1Panel-dev/1Panel/core/utils/xpack"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/robfig/cron/v3"
) )
type BackupService struct{} type BackupService struct{}
@ -36,7 +35,7 @@ type IBackupService interface {
GetLocalDir() (string, error) GetLocalDir() (string, error)
LoadBackupOptions() ([]dto.BackupOption, error) LoadBackupOptions() ([]dto.BackupOption, error)
SearchWithPage(search dto.SearchPageWithType) (int64, interface{}, error) SearchWithPage(search dto.SearchPageWithType) (int64, interface{}, error)
LoadOneDriveInfo() (dto.OneDriveInfo, error) LoadBackupClientInfo(clientType string) (dto.BackupClientInfo, error)
Create(backupDto dto.BackupOperate) error Create(backupDto dto.BackupOperate) error
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
Update(req dto.BackupOperate) 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)) 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{}) varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(item.Vars), &varMap); err != nil { if err := json.Unmarshal([]byte(item.Vars), &varMap); err != nil {
continue continue
@ -171,10 +170,18 @@ func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, inter
return count, data, nil return count, data, nil
} }
func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) { func (u *BackupService) LoadBackupClientInfo(clientType string) (dto.BackupClientInfo, error) {
var data dto.OneDriveInfo 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 data.RedirectUri = constant.OneDriveRedirectURI
clientID, err := settingRepo.Get(commonRepo.WithByKey("OneDriveID")) }
clientID, err := settingRepo.Get(commonRepo.WithByKey(clientIDKey))
if err != nil { if err != nil {
return data, err return data, err
} }
@ -183,7 +190,7 @@ func (u *BackupService) LoadOneDriveInfo() (dto.OneDriveInfo, error) {
return data, err return data, err
} }
data.ClientID = string(idItem) data.ClientID = string(idItem)
clientSecret, err := settingRepo.Get(commonRepo.WithByKey("OneDriveSc")) clientSecret, err := settingRepo.Get(commonRepo.WithByKey(clientIDSc))
if err != nil { if err != nil {
return data, err return data, err
} }
@ -215,8 +222,8 @@ func (u *BackupService) Create(req dto.BackupOperate) error {
} }
backup.Credential = string(itemCredential) backup.Credential = string(itemCredential)
if req.Type == constant.OneDrive { if req.Type == constant.OneDrive || req.Type == constant.GoogleDrive {
if err := u.loadAccessToken(&backup); err != nil { if err := u.loadRefreshTokenByCode(&backup); err != nil {
return err 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) 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) backup.AccessKey, err = encrypt.StringEncrypt(backup.AccessKey)
if err != nil { if err != nil {
@ -284,9 +286,6 @@ func (u *BackupService) Delete(id uint) error {
if backup.Type == constant.Local { if backup.Type == constant.Local {
return buserr.New(constant.ErrBackupLocalDelete) 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 { 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) global.LOG.Errorf("check used of local cronjob failed, err: %v", err)
return buserr.New(constant.ErrBackupInUsed) return buserr.New(constant.ErrBackupInUsed)
@ -338,9 +337,8 @@ func (u *BackupService) Update(req dto.BackupOperate) error {
} }
} }
if newBackup.Type == constant.OneDrive { if newBackup.Type == constant.OneDrive || newBackup.Type == constant.GoogleDrive {
global.Cron.Remove(cron.EntryID(backup.EntryID)) if err := u.loadRefreshTokenByCode(&backup); err != nil {
if err := u.loadAccessToken(&backup); err != nil {
return err return err
} }
} }
@ -392,15 +390,24 @@ func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.Cl
return backClient, nil return backClient, nil
} }
func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error { func (u *BackupService) loadRefreshTokenByCode(backup *model.BackupAccount) error {
varMap := make(map[string]interface{}) varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil {
return fmt.Errorf("unmarshal backup vars failed, err: %v", err) return fmt.Errorf("unmarshal backup vars failed, err: %v", err)
} }
refreshToken, err := client.RefreshToken("authorization_code", "refreshToken", varMap) refreshToken := ""
var err error
if backup.Type == constant.GoogleDrive {
refreshToken, err = client.RefreshGoogleToken("authorization_code", "refreshToken", varMap)
if err != nil { if err != nil {
return err return err
} }
} else {
refreshToken, err = client.RefreshToken("authorization_code", "refreshToken", varMap)
if err != nil {
return err
}
}
delete(varMap, "code") delete(varMap, "code")
varMap["refresh_status"] = constant.StatusSuccess varMap["refresh_status"] = constant.StatusSuccess
varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout)
@ -489,87 +496,59 @@ func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, erro
return client.Upload(fileItem, targetPath) return client.Upload(fileItem, targetPath)
} }
func StartRefreshOneDriveToken(backup *model.BackupAccount) error { func StartRefreshForToken() error {
service := NewIBackupService() service := NewIBackupService()
oneDriveCronID, err := global.Cron.AddJob("0 3 */31 * *", service) refreshID, err := global.Cron.AddJob("0 3 */31 * *", service)
if err != nil { 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 return err
} }
backup.EntryID = uint(oneDriveCronID) global.BackupAccountTokenEntryID = refreshID
return nil return nil
} }
func (u *BackupService) Run() { func (u *BackupService) Run() {
refreshOneDrive() refreshToken()
refreshALIYUN()
} }
func refreshOneDrive() { func refreshToken() {
var backups []model.BackupAccount 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 { for _, backupItem := range backups {
if backupItem.ID == 0 { if backupItem.ID == 0 {
return continue
} }
global.LOG.Infof("start to refresh token of OneDrive %s ...", backupItem.Name)
varMap := make(map[string]interface{}) varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backupItem.Vars), &varMap); err != nil { if err := json.Unmarshal([]byte(backupItem.Vars), &varMap); err != nil {
global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) global.LOG.Errorf("Failed to refresh %s - %s token, please retry, err: %v", backupItem.Type, backupItem.Name, err)
return 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 { if err != nil {
varMap["refresh_status"] = constant.StatusFailed varMap["refresh_status"] = constant.StatusFailed
varMap["refresh_msg"] = err.Error() varMap["refresh_msg"] = err.Error()
global.LOG.Errorf("Failed to refresh OneDrive token, please retry, err: %v", err) 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_status"] = constant.StatusSuccess
varMap["refresh_time"] = time.Now().Format(constant.DateTimeLayout) 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 varMap["refresh_token"] = refreshToken
varsItem, _ := json.Marshal(varMap) varsItem, _ := json.Marshal(varMap)
_ = global.DB.Model(&model.BackupAccount{}). _ = global.DB.Model(&model.BackupAccount{}).Where("id = ?", backupItem.ID).Updates(map[string]interface{}{"vars": varsItem}).Error
Where("id = ?", backupItem.ID).
Updates(map[string]interface{}{
"vars": varsItem,
}).Error
global.LOG.Info("Successfully refreshed ALIYUN token.")
} }
} }

View File

@ -30,5 +30,8 @@ const (
Local = "LOCAL" Local = "LOCAL"
UPYUN = "UPYUN" UPYUN = "UPYUN"
ALIYUN = "ALIYUN" ALIYUN = "ALIYUN"
GoogleDrive = "GoogleDrive"
OneDriveRedirectURI = "http://localhost/login/authorized" OneDriveRedirectURI = "http://localhost/login/authorized"
GoogleRedirectURI = "http://localhost:8080"
) )

View File

@ -26,4 +26,6 @@ var (
I18n *i18n.Localizer I18n *i18n.Localizer
Cron *cron.Cron Cron *cron.Cron
BackupAccountTokenEntryID cron.EntryID
) )

View File

@ -3,7 +3,6 @@ package cron
import ( import (
"time" "time"
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/app/service" "github.com/1Panel-dev/1Panel/core/app/service"
"github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/utils/common" "github.com/1Panel-dev/1Panel/core/utils/common"
@ -14,10 +13,6 @@ func Init() {
nyc, _ := time.LoadLocation(common.LoadTimeZone()) nyc, _ := time.LoadLocation(common.LoadTimeZone())
global.Cron = cron.New(cron.WithLocation(nyc), cron.WithChain(cron.Recover(cron.DefaultLogger)), cron.WithChain(cron.DelayIfStillRunning(cron.DefaultLogger))) global.Cron = cron.New(cron.WithLocation(nyc), cron.WithChain(cron.Recover(cron.DefaultLogger)), cron.WithChain(cron.DelayIfStillRunning(cron.DefaultLogger)))
var accounts []model.BackupAccount _ = service.StartRefreshForToken()
_ = global.DB.Where("type = ?", "OneDrive").Find(&accounts).Error
for i := 0; i < len(accounts); i++ {
_ = service.StartRefreshOneDriveToken(&accounts[i])
}
global.Cron.Start() global.Cron.Start()
} }

View File

@ -17,6 +17,7 @@ func Init() {
migrations.InitTerminalSetting, migrations.InitTerminalSetting,
migrations.InitAppLauncher, migrations.InitAppLauncher,
migrations.InitBackup, migrations.InitBackup,
migrations.InitGoogle,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)

View File

@ -247,3 +247,16 @@ var InitBackup = &gormigrate.Migration{
return tx.AutoMigrate(&model.BackupAccount{}) 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
},
}

View File

@ -19,10 +19,9 @@ type aliClient struct {
} }
func NewALIClient(vars map[string]interface{}) (*aliClient, error) { func NewALIClient(vars map[string]interface{}) (*aliClient, error) {
refresh_token := loadParamFromVars("refresh_token", vars)
drive_id := loadParamFromVars("drive_id", vars) drive_id := loadParamFromVars("drive_id", vars)
token, err := RefreshALIToken(refresh_token) token, err := RefreshALIToken(vars)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -278,7 +277,11 @@ type tokenResp struct {
AccessToken string `json:"access_token"` 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() client := resty.New()
data := map[string]interface{}{ data := map[string]interface{}{
"grant_type": "refresh_token", "grant_type": "refresh_token",

View File

@ -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
}

View File

@ -34,6 +34,8 @@ func NewCloudStorageClient(backupType string, vars map[string]interface{}) (Clou
return client.NewUpClient(vars) return client.NewUpClient(vars)
case constant.ALIYUN: case constant.ALIYUN:
return client.NewALIClient(vars) return client.NewALIClient(vars)
case constant.GoogleDrive:
return client.NewGoogleDriveClient(vars)
default: default:
return nil, constant.ErrNotSupportType return nil, constant.ErrNotSupportType
} }

View File

@ -44,8 +44,8 @@ export const getLocalBackupDir = () => {
export const searchBackup = (params: Backup.SearchWithType) => { export const searchBackup = (params: Backup.SearchWithType) => {
return http.post<ResPage<Backup.BackupInfo>>(`/core/backup/search`, params); return http.post<ResPage<Backup.BackupInfo>>(`/core/backup/search`, params);
}; };
export const getOneDriveInfo = () => { export const getClientInfo = (clientType: string) => {
return http.get<Backup.OneDriveInfo>(`/core/backup/onedrive`); return http.get<Backup.OneDriveInfo>(`/core/backup/client/${clientType}`);
}; };
export const addBackup = (params: Backup.BackupOperate) => { export const addBackup = (params: Backup.BackupOperate) => {
let request = deepCopy(params) as Backup.BackupOperate; let request = deepCopy(params) as Backup.BackupOperate;

View File

@ -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.', 'The current maximum limit for non-client downloads on Aliyun Drive is 100 MB. Exceeding this limit requires downloading through the client.',
ALIYUNRecover: 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.', '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', analysis: 'Analysis',
analysisHelper: analysisHelper:
'Paste the entire token content to automatically parse the required parts. For specific operations, please refer to the official documentation.', '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!', codeWarning: 'The current authorization code format is incorrect, please confirm again!',
code: 'Auth code', code: 'Auth code',
codeHelper: 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', loadCode: 'Acquire',
COS: 'Tencent COS', COS: 'Tencent COS',
ap_beijing_1: 'Beijing Zone 1', ap_beijing_1: 'Beijing Zone 1',

View File

@ -1369,6 +1369,7 @@ const message = {
ALIYUNHelper: '當前阿里雲盤非客戶端下載最大限制為 100 MB超過限制需要通過客戶端下載', ALIYUNHelper: '當前阿里雲盤非客戶端下載最大限制為 100 MB超過限制需要通過客戶端下載',
ALIYUNRecover: ALIYUNRecover:
'當前阿里雲盤非客戶端下載最大限制為 100 MB超過限制需要通過客戶端下載到本地後同步快照進行恢復', '當前阿里雲盤非客戶端下載最大限制為 100 MB超過限制需要通過客戶端下載到本地後同步快照進行恢復',
GoogleDrive: '谷歌云盘',
analysis: '解析', analysis: '解析',
analysisHelper: '粘貼整個 token 內容自動解析所需部分具體操作可參考官方文檔', analysisHelper: '粘貼整個 token 內容自動解析所需部分具體操作可參考官方文檔',
serviceName: '服務名稱', serviceName: '服務名稱',
@ -1387,7 +1388,7 @@ const message = {
backupDir: '備份目录', backupDir: '備份目录',
code: '授權碼', code: '授權碼',
codeHelper: codeHelper:
'請點擊獲取按鈕然後登錄 OneDrive 復製跳轉鏈接中 code 後面的內容粘貼到該輸入框中具體操作可參考官方文檔', '請點擊獲取按鈕然後登錄 {0} 復製跳轉鏈接中 code 後面的內容粘貼到該輸入框中具體操作可參考官方文檔',
loadCode: '獲取', loadCode: '獲取',
COS: '騰訊雲 COS', COS: '騰訊雲 COS',
ap_beijing_1: '北京一區', ap_beijing_1: '北京一區',

View File

@ -1371,6 +1371,7 @@ const message = {
ALIYUNHelper: '当前阿里云盘非客户端下载最大限制为 100 MB超过限制需要通过客户端下载', ALIYUNHelper: '当前阿里云盘非客户端下载最大限制为 100 MB超过限制需要通过客户端下载',
ALIYUNRecover: ALIYUNRecover:
'当前阿里云盘非客户端下载最大限制为 100 MB超过限制需要通过客户端下载到本地后同步快照进行恢复', '当前阿里云盘非客户端下载最大限制为 100 MB超过限制需要通过客户端下载到本地后同步快照进行恢复',
GoogleDrive: '谷歌云盘',
analysis: '解析', analysis: '解析',
analysisHelper: '粘贴整个 token 内容自动解析所需部分具体操作可参考官方文档', analysisHelper: '粘贴整个 token 内容自动解析所需部分具体操作可参考官方文档',
serviceName: '服务名称', serviceName: '服务名称',
@ -1389,7 +1390,7 @@ const message = {
backupDir: '备份目录', backupDir: '备份目录',
code: '授权码', code: '授权码',
codeHelper: codeHelper:
'请点击获取按钮然后登录 OneDrive 复制跳转链接中 code 后面的内容粘贴到该输入框中具体操作可参考官方文档', '请点击获取按钮然后登录 {0} 复制跳转链接中 code 后面的内容粘贴到该输入框中具体操作可参考官方文档',
loadCode: '获取', loadCode: '获取',
COS: '腾讯云 COS', COS: '腾讯云 COS',
ap_beijing_1: '北京一区', ap_beijing_1: '北京一区',

View File

@ -18,6 +18,7 @@
<el-option :label="$t('setting.WebDAV')" value="WebDAV"></el-option> <el-option :label="$t('setting.WebDAV')" value="WebDAV"></el-option>
<el-option :label="$t('setting.UPYUN')" value="UPYUN"></el-option> <el-option :label="$t('setting.UPYUN')" value="UPYUN"></el-option>
<el-option :label="$t('setting.ALIYUN')" value="ALIYUN"></el-option> <el-option :label="$t('setting.ALIYUN')" value="ALIYUN"></el-option>
<el-option :label="$t('setting.GoogleDrive')" value="GoogleDrive"></el-option>
</el-select> </el-select>
<span v-if="isALIYUNYUN()" class="input-help">{{ $t('setting.ALIYUNHelper') }}</span> <span v-if="isALIYUNYUN()" class="input-help">{{ $t('setting.ALIYUNHelper') }}</span>
</el-form-item> </el-form-item>
@ -242,7 +243,7 @@
clearable clearable
v-model.trim="dialogData.rowData!.varsJson['token']" v-model.trim="dialogData.rowData!.varsJson['token']"
/> />
<el-button class="append-button" @click="loadFromToken()"> <el-button class="append-button" @click="loadFromTokenForAliyun()">
{{ $t('setting.analysis') }} {{ $t('setting.analysis') }}
</el-button> </el-button>
<span class="input-help"> <span class="input-help">
@ -266,9 +267,9 @@
</el-form-item> </el-form-item>
</div> </div>
<div v-if="dialogData.rowData!.type === 'OneDrive'"> <div v-if="hasClient()">
<el-form-item> <el-form-item v-if="isOneDrive()">
<el-radio-group v-model="dialogData.rowData!.varsJson['isCN']" @change="changeFrom"> <el-radio-group v-model="dialogData.rowData!.varsJson['isCN']" @change="changeOnedriveFrom">
<el-radio-button :value="false">{{ $t('setting.isNotCN') }}</el-radio-button> <el-radio-button :value="false">{{ $t('setting.isNotCN') }}</el-radio-button>
<el-radio-button :value="true">{{ $t('setting.isCN') }}</el-radio-button> <el-radio-button :value="true">{{ $t('setting.isCN') }}</el-radio-button>
</el-radio-group> </el-radio-group>
@ -310,12 +311,12 @@
clearable clearable
v-model.trim="dialogData.rowData!.varsJson['code']" v-model.trim="dialogData.rowData!.varsJson['code']"
/> />
<el-button class="append-button" @click="jumpAzure(formRef)"> <el-button class="append-button" @click="jumpForCode(formRef)">
{{ $t('setting.loadCode') }} {{ $t('setting.loadCode') }}
</el-button> </el-button>
</div> </div>
<span class="input-help"> <span class="input-help">
{{ $t('setting.codeHelper') }} {{ $t('setting.codeHelper', [$t('setting.' + dialogData.rowData?.type)]) }}
<el-link <el-link
style="font-size: 12px; margin-left: 5px" style="font-size: 12px; margin-left: 5px"
icon="Position" icon="Position"
@ -368,7 +369,7 @@ import { Rules } from '@/global/form-rules';
import i18n from '@/lang'; import i18n from '@/lang';
import { ElForm } from 'element-plus'; import { ElForm } from 'element-plus';
import { Backup } from '@/api/interface/backup'; import { Backup } from '@/api/interface/backup';
import { addBackup, editBackup, getOneDriveInfo, listBucket } from '@/api/modules/backup'; import { addBackup, editBackup, getClientInfo, listBucket } from '@/api/modules/backup';
import { cities } from './../helper'; import { cities } from './../helper';
import { deepCopy, spliceHttp, splitHttp } from '@/utils/util'; import { deepCopy, spliceHttp, splitHttp } from '@/utils/util';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
@ -392,7 +393,7 @@ function checkDriveCode(rule: any, value: any, callback: any) {
if (!value) { if (!value) {
return callback(new Error(i18n.global.t('setting.codeWarning'))); return callback(new Error(i18n.global.t('setting.codeWarning')));
} }
const reg = /^[A-Za-z0-9_.-]+$/; const reg = /^[A-Za-z0-9/_.-]+$/;
if (!reg.test(value)) { if (!reg.test(value)) {
return callback(new Error(i18n.global.t('setting.codeWarning'))); return callback(new Error(i18n.global.t('setting.codeWarning')));
} }
@ -443,7 +444,7 @@ const toDoc = (isConf: boolean) => {
const toWebDAVDoc = () => { const toWebDAVDoc = () => {
window.open('https://1panel.cn/docs/user_manual/settings/#webdav-alist', '_blank', 'noopener,noreferrer'); 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; if (!formEl) return;
const result = await formEl.validateField('varsJson.client_id', callback); const result = await formEl.validateField('varsJson.client_id', callback);
if (!result) { if (!result) {
@ -455,12 +456,18 @@ const jumpAzure = async (formEl: FormInstance | undefined) => {
} }
let client_id = dialogData.value.rowData.varsJson['client_id']; let client_id = dialogData.value.rowData.varsJson['client_id'];
let redirect_uri = dialogData.value.rowData.varsJson['redirect_uri']; let redirect_uri = dialogData.value.rowData.varsJson['redirect_uri'];
if (isOneDrive()) {
let commonUrl = `response_type=code&client_id=${client_id}&redirect_uri=${redirect_uri}&scope=offline_access+Files.ReadWrite.All+User.Read`; 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']) { if (!dialogData.value.rowData!.varsJson['isCN']) {
window.open('https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + commonUrl, '_blank'); window.open('https://login.microsoftonline.com/common/oauth2/v2.0/authorize?' + commonUrl, '_blank');
} else { } else {
window.open('https://login.chinacloudapi.cn/common/oauth2/v2.0/authorize?' + commonUrl, '_blank'); 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) { function callback(error: any) {
if (error) { if (error) {
@ -470,7 +477,7 @@ function callback(error: any) {
} }
} }
const loadFromToken = () => { const loadFromTokenForAliyun = () => {
const obj = JSON.parse(dialogData.value.rowData!.varsJson['token']); const obj = JSON.parse(dialogData.value.rowData!.varsJson['token']);
dialogData.value.rowData!.varsJson['drive_id'] = obj.default_drive_id; dialogData.value.rowData!.varsJson['drive_id'] = obj.default_drive_id;
dialogData.value.rowData!.varsJson['refresh_token'] = obj.refresh_token; dialogData.value.rowData!.varsJson['refresh_token'] = obj.refresh_token;
@ -479,9 +486,18 @@ const hasRemember = () => {
return ( return (
dialogData.value.rowData!.type !== 'LOCAL' && dialogData.value.rowData!.type !== 'LOCAL' &&
dialogData.value.rowData!.type !== 'OneDrive' && 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 = () => { const isUPYUN = () => {
let itemType = dialogData.value.rowData!.type; let itemType = dialogData.value.rowData!.type;
return itemType === 'UPYUN'; return itemType === 'UPYUN';
@ -522,7 +538,7 @@ const changeType = async () => {
break; break;
case 'OneDrive': case 'OneDrive':
dialogData.value.rowData.varsJson['isCN'] = false; dialogData.value.rowData.varsJson['isCN'] = false;
const res = await getOneDriveInfo(); const res = await getClientInfo('Onedrive');
oneDriveInfo.value = res.data; oneDriveInfo.value = res.data;
if (!dialogData.value.rowData.id) { if (!dialogData.value.rowData.id) {
dialogData.value.rowData.varsJson = { dialogData.value.rowData.varsJson = {
@ -532,12 +548,22 @@ const changeType = async () => {
redirect_uri: res.data.redirect_uri, 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': case 'SFTP':
dialogData.value.rowData.varsJson['port'] = 22; dialogData.value.rowData.varsJson['port'] = 22;
dialogData.value.rowData.varsJson['authMode'] = 'password'; dialogData.value.rowData.varsJson['authMode'] = 'password';
} }
}; };
const changeFrom = () => { const changeOnedriveFrom = () => {
if (dialogData.value.rowData.varsJson['isCN']) { if (dialogData.value.rowData.varsJson['isCN']) {
dialogData.value.rowData.varsJson = { dialogData.value.rowData.varsJson = {
isCN: true, isCN: true,