mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 00:09:16 +08:00
feat: 备份账号增加 Google Drive (#7004)
This commit is contained in:
parent
9e82e2837d
commit
03e9077c9a
@ -15,6 +15,7 @@ const (
|
||||
Local = "LOCAL"
|
||||
UPYUN = "UPYUN"
|
||||
ALIYUN = "ALIYUN"
|
||||
GoogleDrive = "GoogleDrive"
|
||||
|
||||
OneDriveRedirectURI = "http://localhost/login/authorized"
|
||||
)
|
||||
|
@ -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
|
||||
|
10
agent/go.sum
10
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=
|
||||
|
409
agent/utils/cloud_storage/client/google_drive.go
Normal file
409
agent/utils/cloud_storage/client/google_drive.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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"`
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -26,4 +26,6 @@ var (
|
||||
I18n *i18n.Localizer
|
||||
|
||||
Cron *cron.Cron
|
||||
|
||||
BackupAccountTokenEntryID cron.EntryID
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ func Init() {
|
||||
migrations.InitTerminalSetting,
|
||||
migrations.InitAppLauncher,
|
||||
migrations.InitBackup,
|
||||
migrations.InitGoogle,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
|
@ -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",
|
||||
|
231
core/utils/cloud_storage/client/google_drive.go
Normal file
231
core/utils/cloud_storage/client/google_drive.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -44,8 +44,8 @@ export const getLocalBackupDir = () => {
|
||||
export const searchBackup = (params: Backup.SearchWithType) => {
|
||||
return http.post<ResPage<Backup.BackupInfo>>(`/core/backup/search`, params);
|
||||
};
|
||||
export const getOneDriveInfo = () => {
|
||||
return http.get<Backup.OneDriveInfo>(`/core/backup/onedrive`);
|
||||
export const getClientInfo = (clientType: string) => {
|
||||
return http.get<Backup.OneDriveInfo>(`/core/backup/client/${clientType}`);
|
||||
};
|
||||
export const addBackup = (params: Backup.BackupOperate) => {
|
||||
let request = deepCopy(params) as Backup.BackupOperate;
|
||||
|
@ -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',
|
||||
|
@ -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: '北京一區',
|
||||
|
@ -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: '北京一区',
|
||||
|
@ -18,6 +18,7 @@
|
||||
<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.ALIYUN')" value="ALIYUN"></el-option>
|
||||
<el-option :label="$t('setting.GoogleDrive')" value="GoogleDrive"></el-option>
|
||||
</el-select>
|
||||
<span v-if="isALIYUNYUN()" class="input-help">{{ $t('setting.ALIYUNHelper') }}</span>
|
||||
</el-form-item>
|
||||
@ -242,7 +243,7 @@
|
||||
clearable
|
||||
v-model.trim="dialogData.rowData!.varsJson['token']"
|
||||
/>
|
||||
<el-button class="append-button" @click="loadFromToken()">
|
||||
<el-button class="append-button" @click="loadFromTokenForAliyun()">
|
||||
{{ $t('setting.analysis') }}
|
||||
</el-button>
|
||||
<span class="input-help">
|
||||
@ -266,9 +267,9 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-if="dialogData.rowData!.type === 'OneDrive'">
|
||||
<el-form-item>
|
||||
<el-radio-group v-model="dialogData.rowData!.varsJson['isCN']" @change="changeFrom">
|
||||
<div v-if="hasClient()">
|
||||
<el-form-item v-if="isOneDrive()">
|
||||
<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="true">{{ $t('setting.isCN') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
@ -310,12 +311,12 @@
|
||||
clearable
|
||||
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') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span class="input-help">
|
||||
{{ $t('setting.codeHelper') }}
|
||||
{{ $t('setting.codeHelper', [$t('setting.' + dialogData.rowData?.type)]) }}
|
||||
<el-link
|
||||
style="font-size: 12px; margin-left: 5px"
|
||||
icon="Position"
|
||||
@ -368,7 +369,7 @@ import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm } from 'element-plus';
|
||||
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 { deepCopy, spliceHttp, splitHttp } from '@/utils/util';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
@ -392,7 +393,7 @@ function checkDriveCode(rule: any, value: any, callback: any) {
|
||||
if (!value) {
|
||||
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)) {
|
||||
return callback(new Error(i18n.global.t('setting.codeWarning')));
|
||||
}
|
||||
@ -443,7 +444,7 @@ const toDoc = (isConf: boolean) => {
|
||||
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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user