From 3fa4a240f769c63531ddf535b4e41ea04b33405f Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Fri, 23 Jun 2023 23:06:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=87=E4=BB=BD=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20Onedrive=20(#1421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/dto/backup.go | 12 +- backend/app/model/backup.go | 1 + backend/app/service/backup.go | 79 ++++- backend/app/service/cornjob.go | 4 +- backend/app/service/cronjob_helper.go | 55 ++- backend/constant/backup.go | 5 + backend/init/migration/migrate.go | 1 + backend/init/migration/migrations/init.go | 10 + .../utils/cloud_storage/client/onedrive.go | 327 ++++++++++++++++++ .../cloud_storage/cloud_storage_client.go | 25 +- frontend/src/api/interface/backup.ts | 2 + frontend/src/assets/iconfont/iconfont.css | 12 +- frontend/src/assets/iconfont/iconfont.js | 2 +- frontend/src/assets/iconfont/iconfont.json | 7 + frontend/src/assets/iconfont/iconfont.svg | 2 + frontend/src/assets/iconfont/iconfont.ttf | Bin 14584 -> 14756 bytes frontend/src/assets/iconfont/iconfont.woff | Bin 9036 -> 9180 bytes frontend/src/assets/iconfont/iconfont.woff2 | Bin 7572 -> 7728 bytes frontend/src/lang/modules/en.ts | 5 + frontend/src/lang/modules/zh.ts | 5 + .../views/setting/backup-account/index.vue | 86 ++++- .../setting/backup-account/operate/index.vue | 32 ++ go.mod | 12 +- go.sum | 20 +- 24 files changed, 644 insertions(+), 60 deletions(-) create mode 100644 backend/utils/cloud_storage/client/onedrive.go diff --git a/backend/app/dto/backup.go b/backend/app/dto/backup.go index e26c4a7cb..90a543dc9 100644 --- a/backend/app/dto/backup.go +++ b/backend/app/dto/backup.go @@ -8,15 +8,17 @@ type BackupOperate struct { Bucket string `json:"bucket"` AccessKey string `json:"accessKey"` Credential string `json:"credential"` + BackupPath string `json:"backupPath"` Vars string `json:"vars" validate:"required"` } type BackupInfo struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Type string `json:"type"` - Bucket string `json:"bucket"` - Vars string `json:"vars"` + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Type string `json:"type"` + Bucket string `json:"bucket"` + BackupPath string `json:"backupPath"` + Vars string `json:"vars"` } type BackupSearch struct { diff --git a/backend/app/model/backup.go b/backend/app/model/backup.go index 70f16da28..7c2fdf80b 100644 --- a/backend/app/model/backup.go +++ b/backend/app/model/backup.go @@ -6,6 +6,7 @@ type BackupAccount struct { Bucket string `gorm:"type:varchar(256)" json:"bucket"` AccessKey string `gorm:"type:varchar(256)" json:"accessKey"` Credential string `gorm:"type:varchar(256)" json:"credential"` + BackupPath string `gorm:"type:varchar(256)" json:"backupPath"` Vars string `gorm:"type:longText" json:"vars"` } diff --git a/backend/app/service/backup.go b/backend/app/service/backup.go index c5a2073ff..7538094b0 100644 --- a/backend/app/service/backup.go +++ b/backend/app/service/backup.go @@ -4,6 +4,9 @@ import ( "context" "encoding/json" "fmt" + "io" + "net/http" + "net/url" "os" "path" "strings" @@ -62,6 +65,7 @@ func (u *BackupService) List() ([]dto.BackupInfo, error) { dtobas = append(dtobas, u.loadByType("MINIO", ops)) dtobas = append(dtobas, u.loadByType("COS", ops)) dtobas = append(dtobas, u.loadByType("KODO", ops)) + dtobas = append(dtobas, u.loadByType("OneDrive", ops)) return dtobas, err } @@ -96,7 +100,6 @@ func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { return "", err } - varMap["type"] = backup.Type varMap["bucket"] = backup.Bucket switch backup.Type { case constant.Sftp: @@ -105,8 +108,10 @@ func (u *BackupService) DownloadRecord(info dto.DownloadRecord) (string, error) case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: varMap["accessKey"] = backup.AccessKey varMap["secretKey"] = backup.Credential + case constant.OneDrive: + varMap["accessToken"] = backup.Credential } - backClient, err := cloud_storage.NewCloudStorageClient(varMap) + backClient, err := cloud_storage.NewCloudStorageClient(backup.Type, varMap) if err != nil { return "", fmt.Errorf("new cloud storage client failed, err: %v", err) } @@ -134,6 +139,12 @@ func (u *BackupService) Create(backupDto dto.BackupOperate) error { if err := copier.Copy(&backup, &backupDto); err != nil { return errors.WithMessage(constant.ErrStructTransform, err.Error()) } + + if backupDto.Type == constant.OneDrive { + if err := u.loadAccessToken(&backup); err != nil { + return err + } + } if err := backupRepo.Create(&backup); err != nil { return err } @@ -145,7 +156,6 @@ func (u *BackupService) GetBuckets(backupDto dto.ForBuckets) ([]interface{}, err if err := json.Unmarshal([]byte(backupDto.Vars), &varMap); err != nil { return nil, err } - varMap["type"] = backupDto.Type switch backupDto.Type { case constant.Sftp: varMap["username"] = backupDto.AccessKey @@ -154,7 +164,7 @@ func (u *BackupService) GetBuckets(backupDto dto.ForBuckets) ([]interface{}, err varMap["accessKey"] = backupDto.AccessKey varMap["secretKey"] = backupDto.Credential } - client, err := cloud_storage.NewCloudStorageClient(varMap) + client, err := cloud_storage.NewCloudStorageClient(backupDto.Type, varMap) if err != nil { return nil, err } @@ -215,6 +225,15 @@ func (u *BackupService) Update(req dto.BackupOperate) error { upMap["bucket"] = req.Bucket upMap["credential"] = req.Credential upMap["vars"] = req.Vars + backup.Vars = req.Vars + + if req.Type == constant.OneDrive { + if err := u.loadAccessToken(&backup); err != nil { + return err + } + upMap["credential"] = backup.Credential + upMap["vars"] = backup.Vars + } if err := backupRepo.Update(req.ID, upMap); err != nil { return err } @@ -251,7 +270,6 @@ func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.Cl if err := json.Unmarshal([]byte(backup.Vars), &varMap); err != nil { return nil, err } - varMap["type"] = backup.Type if backup.Type == "LOCAL" { return nil, errors.New("not support") } @@ -263,9 +281,11 @@ func (u *BackupService) NewClient(backup *model.BackupAccount) (cloud_storage.Cl case constant.OSS, constant.S3, constant.MinIo, constant.Cos, constant.Kodo: varMap["accessKey"] = backup.AccessKey varMap["secretKey"] = backup.Credential + case constant.OneDrive: + varMap["accessToken"] = backup.Credential } - backClient, err := cloud_storage.NewCloudStorageClient(varMap) + backClient, err := cloud_storage.NewCloudStorageClient(backup.Type, varMap) if err != nil { return nil, err } @@ -286,6 +306,53 @@ func (u *BackupService) loadByType(accountType string, accounts []model.BackupAc return dto.BackupInfo{Type: accountType} } +func (u *BackupService) loadAccessToken(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) + } + + data := url.Values{} + data.Set("client_id", constant.OneDriveClientID) + data.Set("client_secret", constant.OneDriveClientSecret) + data.Set("grant_type", "authorization_code") + data.Set("code", varMap["code"].(string)) + data.Set("redirect_uri", constant.OneDriveRedirectURI) + client := &http.Client{} + req, err := http.NewRequest("POST", "https://login.microsoftonline.com/common/oauth2/v2.0/token", strings.NewReader(data.Encode())) + if err != nil { + return fmt.Errorf("new http post client for access token failed, err: %v", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("request for access token failed, err: %v", err) + } + delete(varMap, "code") + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read data from response body failed, err: %v", err) + } + defer resp.Body.Close() + + token := map[string]interface{}{} + if err := json.Unmarshal(respBody, &token); err != nil { + return fmt.Errorf("unmarshal data from response body failed, err: %v", err) + } + accessToken, ok := token["refresh_token"].(string) + if !ok { + return errors.New("no such access token in response") + } + + itemVars, err := json.Marshal(varMap) + if err != nil { + return fmt.Errorf("json marshal var map failed, err: %v", err) + } + backup.Credential = accessToken + backup.Vars = string(itemVars) + return nil +} + func loadLocalDir() (string, error) { backup, err := backupRepo.Get(commonRepo.WithByType("LOCAL")) if err != nil { diff --git a/backend/app/service/cornjob.go b/backend/app/service/cornjob.go index 132b10732..ef336993a 100644 --- a/backend/app/service/cornjob.go +++ b/backend/app/service/cornjob.go @@ -100,9 +100,9 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error { if err != nil { return err } - u.HandleRmExpired(backup.Type, localDir, &cronjob, client) + u.HandleRmExpired(backup.Type, backup.BackupPath, localDir, &cronjob, client) } else { - u.HandleRmExpired(backup.Type, "", &cronjob, nil) + u.HandleRmExpired(backup.Type, backup.BackupPath, "", &cronjob, nil) } } delRecords, err := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(req.CronjobID))) diff --git a/backend/app/service/cronjob_helper.go b/backend/app/service/cronjob_helper.go index 17e28429b..922d5b3d2 100644 --- a/backend/app/service/cronjob_helper.go +++ b/backend/app/service/cronjob_helper.go @@ -33,16 +33,16 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) { return } message, err = u.handleShell(cronjob.Type, cronjob.Name, cronjob.Script) - u.HandleRmExpired("LOCAL", "", cronjob, nil) + u.HandleRmExpired("LOCAL", "", "", cronjob, nil) case "curl": if len(cronjob.URL) == 0 { return } message, err = u.handleShell(cronjob.Type, cronjob.Name, fmt.Sprintf("curl '%s'", cronjob.URL)) - u.HandleRmExpired("LOCAL", "", cronjob, nil) + u.HandleRmExpired("LOCAL", "", "", cronjob, nil) case "ntp": err = u.handleNtpSync() - u.HandleRmExpired("LOCAL", "", cronjob, nil) + u.HandleRmExpired("LOCAL", "", "", cronjob, nil) case "website": record.File, err = u.handleBackup(cronjob, record.StartTime) case "database": @@ -142,19 +142,25 @@ func (u *CronjobService) handleBackup(cronjob *model.Cronjob, startTime time.Tim if err != nil { return "", err } + if len(backup.BackupPath) != 0 { + itemPath := strings.TrimPrefix(backup.BackupPath, "/") + itemPath = strings.TrimSuffix(itemPath, "/") + "/" + itemFileDir = itemPath + itemFileDir + } if _, err = client.Upload(backupDir+"/"+fileName, itemFileDir+"/"+fileName); err != nil { return "", err } } - u.HandleRmExpired(backup.Type, localDir, cronjob, client) + u.HandleRmExpired(backup.Type, backup.BackupPath, localDir, cronjob, client) if backup.Type == "LOCAL" || cronjob.KeepLocal { - return fmt.Sprintf("%s/%s/%s/%s", localDir, cronjob.Type, cronjob.Name, fileName), nil + return fmt.Sprintf("%s/%s", backupDir, fileName), nil + } else { + return fmt.Sprintf("%s/%s", itemFileDir, fileName), nil } - return fmt.Sprintf("%s/%s/%s", cronjob.Type, cronjob.Name, fileName), nil } } -func (u *CronjobService) HandleRmExpired(backType, localDir string, cronjob *model.Cronjob, backClient cloud_storage.CloudStorageClient) { +func (u *CronjobService) HandleRmExpired(backType, backupPath, localDir string, cronjob *model.Cronjob, backClient cloud_storage.CloudStorageClient) { global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies) records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID)), commonRepo.WithOrderBy("created_at desc")) if len(records) > int(cronjob.RetainCopies) { @@ -163,7 +169,7 @@ func (u *CronjobService) HandleRmExpired(backType, localDir string, cronjob *mod files := strings.Split(records[i].File, ",") for _, file := range files { if backType != "LOCAL" { - _, _ = backClient.Delete(strings.ReplaceAll(file, localDir+"/", "")) + _, _ = backClient.Delete(backupPath + "/" + strings.TrimPrefix(file, localDir+"/")) _ = os.Remove(file) } else { _ = os.Remove(file) @@ -270,12 +276,11 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, app *repo.RootInf } record.DetailName = dbName record.FileDir = backupDir - itemFileDir := strings.ReplaceAll(backupDir, localDir+"/", "") + itemFileDir := strings.TrimPrefix(backupDir, localDir+"/") if !cronjob.KeepLocal && backup.Type != "LOCAL" { record.Source = backup.Type record.FileDir = itemFileDir } - paths = append(paths, fmt.Sprintf("%s/%s", record.FileDir, record.FileName)) if err := backupRepo.CreateRecord(&record); err != nil { global.LOG.Errorf("save backup record failed, err: %v", err) @@ -287,12 +292,22 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, app *repo.RootInf _ = os.RemoveAll(fmt.Sprintf("%s/%s", backupDir, record.FileName)) }() } + if len(backup.BackupPath) != 0 { + itemPath := strings.TrimPrefix(backup.BackupPath, "/") + itemPath = strings.TrimSuffix(itemPath, "/") + "/" + itemFileDir = itemPath + itemFileDir + } if _, err = client.Upload(backupDir+"/"+record.FileName, itemFileDir+"/"+record.FileName); err != nil { return paths, err } } + if backup.Type == "LOCAL" || cronjob.KeepLocal { + paths = append(paths, fmt.Sprintf("%s/%s", record.FileDir, record.FileName)) + } else { + paths = append(paths, fmt.Sprintf("%s/%s", itemFileDir, record.FileName)) + } } - u.HandleRmExpired(backup.Type, localDir, &cronjob, client) + u.HandleRmExpired(backup.Type, backup.BackupPath, localDir, &cronjob, client) return paths, nil } @@ -358,7 +373,7 @@ func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime t }() } wg.Wait() - u.HandleRmExpired("LOCAL", "", cronjob, nil) + u.HandleRmExpired("LOCAL", "", "", cronjob, nil) return strings.Join(filePaths, ","), nil } @@ -399,10 +414,10 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, backup model.Backu } backupDir := fmt.Sprintf("%s/website/%s", localDir, website.PrimaryDomain) record.FileDir = backupDir - itemFileDir := strings.ReplaceAll(backupDir, localDir+"/", "") + itemFileDir := strings.TrimPrefix(backupDir, localDir+"/") if !cronjob.KeepLocal && backup.Type != "LOCAL" { record.Source = backup.Type - record.FileDir = strings.ReplaceAll(backupDir, localDir+"/", "") + record.FileDir = strings.TrimPrefix(backupDir, localDir+"/") } record.FileName = fmt.Sprintf("website_%s_%s.tar.gz", website.PrimaryDomain, startTime.Format("20060102150405")) paths = append(paths, fmt.Sprintf("%s/%s", record.FileDir, record.FileName)) @@ -420,11 +435,21 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, backup model.Backu _ = os.RemoveAll(fmt.Sprintf("%s/%s", backupDir, record.FileName)) }() } + if len(backup.BackupPath) != 0 { + itemPath := strings.TrimPrefix(backup.BackupPath, "/") + itemPath = strings.TrimSuffix(itemPath, "/") + "/" + itemFileDir = itemPath + itemFileDir + } if _, err = client.Upload(backupDir+"/"+record.FileName, itemFileDir+"/"+record.FileName); err != nil { return paths, err } } + if backup.Type == "LOCAL" || cronjob.KeepLocal { + paths = append(paths, fmt.Sprintf("%s/%s", record.FileDir, record.FileName)) + } else { + paths = append(paths, fmt.Sprintf("%s/%s", itemFileDir, record.FileName)) + } } - u.HandleRmExpired(backup.Type, localDir, &cronjob, client) + u.HandleRmExpired(backup.Type, backup.BackupPath, localDir, &cronjob, client) return paths, nil } diff --git a/backend/constant/backup.go b/backend/constant/backup.go index f2c74c9f4..57dbeed53 100644 --- a/backend/constant/backup.go +++ b/backend/constant/backup.go @@ -7,7 +7,12 @@ const ( S3 = "S3" OSS = "OSS" Sftp = "SFTP" + OneDrive = "OneDrive" MinIo = "MINIO" Cos = "COS" Kodo = "KODO" + + OneDriveClientID = "5446cfe3-4c79-47a0-ae25-fc645478e2d9" + OneDriveClientSecret = "ITh8Q~0UKJNXAvz6HE~pd3DTnGJOgDEEpnDOJbqY" + OneDriveRedirectURI = "http://localhost/login/authorized" ) diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 12eb5a027..2742d23f5 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -31,6 +31,7 @@ func Init() { migrations.AddBindAndAllowIPs, migrations.UpdateCronjobWithSecond, migrations.UpdateWebsite, + migrations.AddBackupAccountDir, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index 5009467aa..d20fc342f 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -390,3 +390,13 @@ var UpdateWebsite = &gormigrate.Migration{ return nil }, } + +var AddBackupAccountDir = &gormigrate.Migration{ + ID: "20200620-add-backup-dir", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.BackupAccount{}); err != nil { + return err + } + return nil + }, +} diff --git a/backend/utils/cloud_storage/client/onedrive.go b/backend/utils/cloud_storage/client/onedrive.go new file mode 100644 index 000000000..7568e8e20 --- /dev/null +++ b/backend/utils/cloud_storage/client/onedrive.go @@ -0,0 +1,327 @@ +package client + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "strconv" + "strings" + + "github.com/1Panel-dev/1Panel/backend/app/model" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" + odsdk "github.com/goh-chunlin/go-onedrive/onedrive" + "golang.org/x/oauth2" +) + +type oneDriveClient struct { + Vars map[string]interface{} + client odsdk.Client +} + +func NewOneDriveClient(vars map[string]interface{}) (*oneDriveClient, error) { + token := "" + if _, ok := vars["accessToken"]; ok { + token = vars["accessToken"].(string) + } else { + return nil, constant.ErrInvalidParams + } + ctx := context.Background() + + newToken, err := refreshToken(token) + if err != nil { + return nil, err + } + _ = global.DB.Model(&model.Group{}).Where("type = ?", "OneDrive").Updates(map[string]interface{}{"credential": newToken}).Error + + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: newToken}, + ) + tc := oauth2.NewClient(ctx, ts) + + client := odsdk.NewClient(tc) + return &oneDriveClient{client: *client}, nil +} + +func (onedrive oneDriveClient) ListBuckets() ([]interface{}, error) { + return nil, nil +} + +func (onedrive oneDriveClient) Exist(path string) (bool, error) { + path = "/" + strings.TrimPrefix(path, "/") + fileID, err := onedrive.loadIDByPath(path) + if err != nil { + return false, err + } + + return len(fileID) != 0, nil +} + +func (onedrive oneDriveClient) Delete(path string) (bool, error) { + path = "/" + strings.TrimPrefix(path, "/") + fileID, err := onedrive.loadIDByPath(path) + if err != nil { + return false, err + } + + if err := onedrive.client.DriveItems.Delete(context.Background(), "", fileID); err != nil { + return false, err + } + return true, nil +} + +func (onedrive oneDriveClient) Upload(src, target string) (bool, error) { + target = "/" + strings.TrimPrefix(target, "/") + if _, err := onedrive.loadIDByPath(path.Dir(target)); err != nil { + if !strings.Contains(err.Error(), "itemNotFound") { + return false, err + } + if err := onedrive.createFolder(path.Dir(target)); err != nil { + return false, fmt.Errorf("create dir before upload failed, err: %v", err) + } + } + + ctx := context.Background() + file, err := os.Open(src) + if err != nil { + return false, err + } + defer file.Close() + fileInfo, err := file.Stat() + if err != nil { + return false, err + } + if fileInfo.IsDir() { + return false, errors.New("Only file is allowed to be uploaded here.") + } + fileName := fileInfo.Name() + fileSize := fileInfo.Size() + + folderID, err := onedrive.loadIDByPath(path.Dir(target)) + if err != nil { + return false, err + } + apiURL := fmt.Sprintf("me/drive/items/%s:/%s:/createUploadSession", url.PathEscape(folderID), fileName) + sessionCreationRequestInside := NewUploadSessionCreationRequest{ + ConflictBehavior: "rename", + } + + sessionCreationRequest := struct { + Item NewUploadSessionCreationRequest `json:"item"` + DeferCommit bool `json:"deferCommit"` + }{sessionCreationRequestInside, false} + + sessionCreationReq, err := onedrive.client.NewRequest("POST", apiURL, sessionCreationRequest) + if err != nil { + return false, err + } + + var sessionCreationResp *NewUploadSessionCreationResponse + err = onedrive.client.Do(ctx, sessionCreationReq, false, &sessionCreationResp) + if err != nil { + return false, fmt.Errorf("session creation failed %w", err) + } + + fileSessionUploadUrl := sessionCreationResp.UploadURL + + sizePerSplit := int64(3200 * 1024) + buffer := make([]byte, 3200*1024) + splitCount := fileSize / sizePerSplit + if fileSize%sizePerSplit != 0 { + splitCount += 1 + } + bfReader := bufio.NewReader(file) + var fileUploadResp *UploadSessionUploadResponse + for splitNow := int64(0); splitNow < splitCount; splitNow++ { + length, err := bfReader.Read(buffer) + if err != nil { + return false, err + } + if int64(length) < sizePerSplit { + bufferLast := buffer[:length] + buffer = bufferLast + } + sessionFileUploadReq, err := onedrive.NewSessionFileUploadRequest(fileSessionUploadUrl, splitNow*sizePerSplit, fileSize, bytes.NewReader(buffer)) + if err != nil { + return false, err + } + if err := onedrive.client.Do(ctx, sessionFileUploadReq, false, &fileUploadResp); err != nil { + return false, err + } + } + if fileUploadResp.Id == "" { + return false, errors.New("something went wrong. file upload incomplete. consider upload the file in a step-by-step manner") + } + + return true, nil +} + +func (onedrive oneDriveClient) Download(src, target string) (bool, error) { + src = "/" + strings.TrimPrefix(src, "/") + req, err := onedrive.client.NewRequest("GET", fmt.Sprintf("me/drive/root:%s", src), nil) + if err != nil { + return false, fmt.Errorf("new request for file id failed, err: %v", err) + } + var driveItem *odsdk.DriveItem + if err := onedrive.client.Do(context.Background(), req, false, &driveItem); err != nil { + return false, fmt.Errorf("do request for file id failed, err: %v", err) + } + + resp, err := http.Get(driveItem.DownloadURL) + if err != nil { + return false, err + } + defer resp.Body.Close() + + out, err := os.Create(target) + if err != nil { + return false, err + } + defer out.Close() + buffer := make([]byte, 2*1024*1024) + + _, err = io.CopyBuffer(out, resp.Body, buffer) + if err != nil { + return false, err + } + + return true, nil +} + +func (onedrive *oneDriveClient) ListObjects(prefix string) ([]interface{}, error) { + prefix = "/" + strings.TrimPrefix(prefix, "/") + folderID, err := onedrive.loadIDByPath(prefix) + if err != nil { + return nil, err + } + + req, err := onedrive.client.NewRequest("GET", fmt.Sprintf("me/drive/items/%s/children", folderID), nil) + if err != nil { + return nil, fmt.Errorf("new request for delete failed, err: %v", err) + } + var driveItems *odsdk.OneDriveDriveItemsResponse + if err := onedrive.client.Do(context.Background(), req, false, &driveItems); err != nil { + return nil, fmt.Errorf("do request for delete failed, err: %v", err) + } + for _, item := range driveItems.DriveItems { + return nil, fmt.Errorf("id: %v, name: %s \n", item.Id, item.Name) + } + + var itemList []interface{} + for _, item := range driveItems.DriveItems { + itemList = append(itemList, item.Name) + } + return itemList, nil +} + +func (onedrive *oneDriveClient) loadIDByPath(path string) (string, error) { + req, err := onedrive.client.NewRequest("GET", fmt.Sprintf("me/drive/root:%s", path), nil) + if err != nil { + return "", fmt.Errorf("new request for file id failed, err: %v", err) + } + var driveItem *odsdk.DriveItem + if err := onedrive.client.Do(context.Background(), req, false, &driveItem); err != nil { + return "", fmt.Errorf("do request for file id failed, err: %v", err) + } + return driveItem.Id, nil +} + +func refreshToken(oldToken string) (string, error) { + data := url.Values{} + data.Set("client_id", constant.OneDriveClientID) + data.Set("client_secret", constant.OneDriveClientSecret) + data.Set("grant_type", "refresh_token") + data.Set("refresh_token", oldToken) + data.Set("redirect_uri", constant.OneDriveRedirectURI) + client := &http.Client{} + req, err := http.NewRequest("POST", "https://login.microsoftonline.com/common/oauth2/v2.0/token", strings.NewReader(data.Encode())) + if err != nil { + return "", fmt.Errorf("new http post client for access token failed, err: %v", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("request for access token failed, err: %v", err) + } + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read data from response body failed, err: %v", err) + } + defer resp.Body.Close() + + tokenMap := map[string]interface{}{} + if err := json.Unmarshal(respBody, &tokenMap); err != nil { + return "", fmt.Errorf("unmarshal data from response body failed, err: %v", err) + } + accessToken, ok := tokenMap["access_token"].(string) + if !ok { + return "", errors.New("no such access token in response") + } + return accessToken, nil +} + +func (onedrive *oneDriveClient) createFolder(parent string) error { + if _, err := onedrive.loadIDByPath(path.Dir(parent)); err != nil { + if !strings.Contains(err.Error(), "itemNotFound") { + return err + } + _ = onedrive.createFolder(path.Dir(parent)) + } + item2, err := onedrive.loadIDByPath(path.Dir(parent)) + if err != nil { + return err + } + if _, err := onedrive.client.DriveItems.CreateNewFolder(context.Background(), "", item2, path.Base(parent)); err != nil { + return err + } + return nil +} + +type NewUploadSessionCreationRequest struct { + ConflictBehavior string `json:"@microsoft.graph.conflictBehavior,omitempty"` +} +type NewUploadSessionCreationResponse struct { + UploadURL string `json:"uploadUrl"` + ExpirationDateTime string `json:"expirationDateTime"` +} +type UploadSessionUploadResponse struct { + ExpirationDateTime string `json:"expirationDateTime"` + NextExpectedRanges []string `json:"nextExpectedRanges"` + DriveItem +} +type DriveItem struct { + Name string `json:"name"` + Id string `json:"id"` + DownloadURL string `json:"@microsoft.graph.downloadUrl"` + Description string `json:"description"` + Size int64 `json:"size"` + WebURL string `json:"webUrl"` +} + +func (onedrive *oneDriveClient) NewSessionFileUploadRequest(absoluteUrl string, grandOffset, grandTotalSize int64, byteReader *bytes.Reader) (*http.Request, error) { + apiUrl, err := onedrive.client.BaseURL.Parse(absoluteUrl) + if err != nil { + return nil, err + } + absoluteUrl = apiUrl.String() + contentLength := byteReader.Size() + req, err := http.NewRequest("PUT", absoluteUrl, byteReader) + req.Header.Set("Content-Length", strconv.FormatInt(contentLength, 10)) + preliminaryLength := grandOffset + preliminaryRange := grandOffset + contentLength - 1 + if preliminaryRange >= grandTotalSize { + preliminaryRange = grandTotalSize - 1 + preliminaryLength = preliminaryRange - grandOffset + 1 + } + req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", preliminaryLength, preliminaryRange, grandTotalSize)) + + return req, err +} diff --git a/backend/utils/cloud_storage/cloud_storage_client.go b/backend/utils/cloud_storage/cloud_storage_client.go index ec5af713e..0abbc4d50 100644 --- a/backend/utils/cloud_storage/cloud_storage_client.go +++ b/backend/utils/cloud_storage/cloud_storage_client.go @@ -14,24 +14,23 @@ type CloudStorageClient interface { Download(src, target string) (bool, error) } -func NewCloudStorageClient(vars map[string]interface{}) (CloudStorageClient, error) { - if vars["type"] == constant.S3 { +func NewCloudStorageClient(backupType string, vars map[string]interface{}) (CloudStorageClient, error) { + switch backupType { + case constant.S3: return client.NewS3Client(vars) - } - if vars["type"] == constant.OSS { + case constant.OSS: return client.NewOssClient(vars) - } - if vars["type"] == constant.Sftp { + case constant.Sftp: return client.NewSftpClient(vars) - } - if vars["type"] == constant.MinIo { + case constant.MinIo: return client.NewMinIoClient(vars) - } - if vars["type"] == constant.Cos { + case constant.Cos: return client.NewCosClient(vars) - } - if vars["type"] == constant.Kodo { + case constant.Kodo: return client.NewKodoClient(vars) + case constant.OneDrive: + return client.NewOneDriveClient(vars) + default: + return nil, constant.ErrNotSupportType } - return nil, constant.ErrNotSupportType } diff --git a/frontend/src/api/interface/backup.ts b/frontend/src/api/interface/backup.ts index 6ee755ed3..af2ac7c6a 100644 --- a/frontend/src/api/interface/backup.ts +++ b/frontend/src/api/interface/backup.ts @@ -7,6 +7,7 @@ export namespace Backup { accessKey: string; bucket: string; credential: string; + backupPath: string; vars: string; varsJson: object; createdAt: Date; @@ -17,6 +18,7 @@ export namespace Backup { accessKey: string; bucket: string; credential: string; + backupPath: string; vars: string; } export interface RecordDownload { diff --git a/frontend/src/assets/iconfont/iconfont.css b/frontend/src/assets/iconfont/iconfont.css index 6505a38ac..447ddad71 100644 --- a/frontend/src/assets/iconfont/iconfont.css +++ b/frontend/src/assets/iconfont/iconfont.css @@ -1,9 +1,9 @@ @font-face { font-family: "panel"; /* Project id 3575356 */ - src: url('iconfont.woff2?t=1684465849452') format('woff2'), - url('iconfont.woff?t=1684465849452') format('woff'), - url('iconfont.ttf?t=1684465849452') format('truetype'), - url('iconfont.svg?t=1684465849452#panel') format('svg'); + src: url('iconfont.woff2?t=1687338712846') format('woff2'), + url('iconfont.woff?t=1687338712846') format('woff'), + url('iconfont.ttf?t=1687338712846') format('truetype'), + url('iconfont.svg?t=1687338712846#panel') format('svg'); } .panel { @@ -14,6 +14,10 @@ -moz-osx-font-smoothing: grayscale; } +.p-onedrive:before { + content: "\e601"; +} + .p-caidan:before { content: "\e61d"; } diff --git a/frontend/src/assets/iconfont/iconfont.js b/frontend/src/assets/iconfont/iconfont.js index b13583654..ccdf3fa1c 100644 --- a/frontend/src/assets/iconfont/iconfont.js +++ b/frontend/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_3575356='',function(c){var l=(l=document.getElementsByTagName("script"))[l.length-1],h=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var a,t,v,p,i,z=function(l,h){h.parentNode.insertBefore(l,h)};if(h&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}a=function(){var l,h=document.createElement("div");h.innerHTML=c._iconfont_svg_string_3575356,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(l=document.body).firstChild?z(h,l.firstChild):l.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),a()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(v=a,p=c.document,i=!1,o(),p.onreadystatechange=function(){"complete"==p.readyState&&(p.onreadystatechange=null,m())})}function m(){i||(i=!0,v())}function o(){try{p.documentElement.doScroll("left")}catch(l){return void setTimeout(o,50)}m()}}(window); \ No newline at end of file +window._iconfont_svg_string_3575356='',function(c){var l=(l=document.getElementsByTagName("script"))[l.length-1],h=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var a,t,v,p,i,z=function(l,h){h.parentNode.insertBefore(l,h)};if(h&&!c.__iconfont__svg__cssinject__){c.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}a=function(){var l,h=document.createElement("div");h.innerHTML=c._iconfont_svg_string_3575356,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(l=document.body).firstChild?z(h,l.firstChild):l.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),a()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(v=a,p=c.document,i=!1,o(),p.onreadystatechange=function(){"complete"==p.readyState&&(p.onreadystatechange=null,m())})}function m(){i||(i=!0,v())}function o(){try{p.documentElement.doScroll("left")}catch(l){return void setTimeout(o,50)}m()}}(window); \ No newline at end of file diff --git a/frontend/src/assets/iconfont/iconfont.json b/frontend/src/assets/iconfont/iconfont.json index 1d602c93b..951ee1a03 100644 --- a/frontend/src/assets/iconfont/iconfont.json +++ b/frontend/src/assets/iconfont/iconfont.json @@ -5,6 +5,13 @@ "css_prefix_text": "p-", "description": "", "glyphs": [ + { + "icon_id": "13015332", + "name": "onedrive", + "font_class": "onedrive", + "unicode": "e601", + "unicode_decimal": 58881 + }, { "icon_id": "7708032", "name": "菜单", diff --git a/frontend/src/assets/iconfont/iconfont.svg b/frontend/src/assets/iconfont/iconfont.svg index 3bccc2519..9b44b1eff 100644 --- a/frontend/src/assets/iconfont/iconfont.svg +++ b/frontend/src/assets/iconfont/iconfont.svg @@ -14,6 +14,8 @@ /> + + diff --git a/frontend/src/assets/iconfont/iconfont.ttf b/frontend/src/assets/iconfont/iconfont.ttf index f3b3ffcf06b53529bea96d5fb22b0d577c0bf4a1..32cd9a78efb382f2537199767874bcd25e0f1554 100644 GIT binary patch delta 1449 zcmaKsU2IfE0EN%oyMNu?cDw!A7W%{fKoRKf*1dHX34vOhVj65fktjxF>9#DUrCVqk zjKmei1f$W?`_e>36B~?%U<`@rLro9~;lVT#W5QEI)Xc5L_+$ybP|tM47$2OQZ|2V2 znR{o>IlK49W=>2d%H4o?2f(3$k$mZ~>AM#IV-|4t9vOalsPD6L=K)_4XunY$%ojFx z#4mB~ZIUf=!2hb}Fz@Hc`r^p=$@wo(kC#SIjPF}+cniplv!6aUm)L1Ra4vDZ_rpgohPE#w!1*k< zXGfoX(60Qs@|x#U^ECTD7;2P7t+VV_F(xYQN%6sy;j-I}_pOE12N1VluHLn?_48{B zaEXgt(vL2prCcBX_kE!?fbie<7nALL|Sw=JB+}OKy9u9ijAecGC-cGoLpJ$Fq1Q=B6En=5TzBg0CvhQXKLD31{=&`}V@uL%im!HplC6=1 delta 1243 zcmaKrOGs2v9LB$U=jqJo%;aQdW!Ctr%+e%Hqy$4-6c|PHkZSUgfv-};BAkMd77=4N zf?m)<2qKEIRY(OvShPu6w1|+Bb1hr6NGJ>KJN^x-Md$Fl_ntH7-gExn_su}(x1)(@ zyb3VR0XSIS8tWLC>fZ;L4**x?;g;h~y^p^?2fTGaK}mCCtReSJcL&#wlJ(7;@b$W5 zd>$g%&8=NMFYeBKFb)JOCgJCLw$XE!H~hZOe4jbxzUes4 zu@|PEWz+8!TbagDBgIM>*Q~YX)uOT4Ss3r&n0+eX-N&uQ;56=YeJv{CLI_RhGS|I| zDp*tI4#TvzI{M@Ki?x6Co%mnhjYI$R2K?PADhjb09_|>%N)%x}f|$pBoLGf4r1EsR zSb;o5uy8#x;Ko`On8YLd5kN9hSnMJ!#u8*=DYCE(%dv(9M3IejD9D)87)T%utCP!wLnQlMM{i-K|s zEDuV*Ls%$Okdmq(2SlX_tR8BKpdC^?h=z(ll?l`aszRVjP&ESeg4!-nIjB7XHH4}c zs3ugSfOsAsP70J5>ZyQ*yblXh9qPS6{h>YxbOLHjpeIn2K|^<-Y(Ym$jSF-P>Z3sK zpgsX{eW8s|69Nr|y;;yXwyOo2411?QyJ7DaXgusA0wVyM{{Ric0Q<~p1SSdgGl7}HIQ`TxT^Mf=m@|w|3rrj)D6a-N1SrQD1IBsd zr&(s6FozuxN1J2B+3hl2duv?JJR#4Bx7_&R= DGwult diff --git a/frontend/src/assets/iconfont/iconfont.woff b/frontend/src/assets/iconfont/iconfont.woff index 9e851344397d6c4230513c210e0a92cd8717cd57..40a4e87d015785c9d8fb79be4db0bf7221a564d1 100644 GIT binary patch delta 8585 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 P={k!30^bsxbN~PV?-780 delta 8439 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 {{ s3Data.bucket }} + + {{ s3Data.backupPath }} + {{ dateFormat(0, 0, s3Data.createdAt) }} @@ -102,6 +105,9 @@ {{ ossData.bucket }} + + {{ ossData.backupPath }} + {{ dateFormat(0, 0, ossData.createdAt) }} @@ -139,6 +145,9 @@ {{ cosData.bucket }} + + {{ cosData.backupPath }} + {{ dateFormat(0, 0, cosData.createdAt) }} @@ -149,6 +158,47 @@ + +

+ +  {{ $t('setting.OneDrive') }} +
+ + {{ $t('commons.button.edit') }} + + + {{ $t('commons.button.delete') }} + +
+
+ +
+ + {{ oneDriveData.backupPath }} + + + {{ dateFormat(0, 0, oneDriveData.createdAt) }} + +
+ + + {{ $t('setting.createBackupAccount', [$t('setting.OneDrive')]) }} + + + + +
@@ -175,6 +225,9 @@ {{ kodoData.bucket }} + + {{ kodoData.backupPath }} + {{ dateFormat(0, 0, kodoData.createdAt) }} @@ -185,8 +238,6 @@ - -
@@ -212,6 +263,9 @@ {{ minioData.bucket }} + + {{ minioData.backupPath }} + {{ dateFormat(0, 0, minioData.createdAt) }} @@ -222,6 +276,8 @@ + +
@@ -251,6 +307,9 @@ {{ sftpData.bucket }} + + {{ sftpData.backupPath }} + {{ dateFormat(0, 0, sftpData.createdAt) }} @@ -284,6 +343,7 @@ const localData = ref({ accessKey: '', bucket: '', credential: '', + backupPath: '', vars: '', varsJson: { dir: '', @@ -296,6 +356,7 @@ const ossData = ref({ accessKey: '', bucket: '', credential: '', + backupPath: '', vars: '', varsJson: { region: '', @@ -309,6 +370,7 @@ const minioData = ref({ accessKey: '', bucket: '', credential: '', + backupPath: '', vars: '', varsJson: { region: '', @@ -322,6 +384,7 @@ const sftpData = ref({ accessKey: '', bucket: '', credential: '', + backupPath: '', vars: '', varsJson: { address: '', @@ -329,12 +392,26 @@ const sftpData = ref({ }, createdAt: new Date(), }); +const oneDriveData = ref({ + id: 0, + type: 'OneDrive', + accessKey: '', + bucket: '', + credential: '', + backupPath: '', + vars: '', + varsJson: { + redirectURI: '', + }, + createdAt: new Date(), +}); const s3Data = ref({ id: 0, type: 'S3', accessKey: '', bucket: '', credential: '', + backupPath: '', vars: '', varsJson: { region: '', @@ -348,6 +425,7 @@ const cosData = ref({ accessKey: '', bucket: '', credential: '', + backupPath: '', vars: '', varsJson: { region: '', @@ -360,6 +438,7 @@ const kodoData = ref({ accessKey: '', bucket: '', credential: '', + backupPath: '', vars: '', varsJson: { domain: '', @@ -396,6 +475,9 @@ const search = async () => { case 'KODO': kodoData.value = bac; break; + case 'OneDrive': + oneDriveData.value = bac; + break; } } }; diff --git a/frontend/src/views/setting/backup-account/operate/index.vue b/frontend/src/views/setting/backup-account/operate/index.vue index 5e89cf434..95325c490 100644 --- a/frontend/src/views/setting/backup-account/operate/index.vue +++ b/frontend/src/views/setting/backup-account/operate/index.vue @@ -45,6 +45,21 @@ > + + + + + + + +
+ + +
@@ -206,6 +228,16 @@ const handleClose = () => { drawerVisiable.value = false; }; +const jumpAzure = () => { + let commonUrl = + 'response_type=code&client_id=5446cfe3-4c79-47a0-ae25-fc645478e2d9&redirect_uri=http://localhost/login/authorized&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'); + } +}; + const loadDir = async (path: string) => { dialogData.value.rowData!.varsJson['dir'] = path; }; diff --git a/go.mod b/go.mod index 2698f2897..c9524a3e0 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/go-gormigrate/gormigrate/v2 v2.0.2 github.com/go-playground/validator/v10 v10.14.0 github.com/go-sql-driver/mysql v1.6.0 + github.com/goh-chunlin/go-onedrive v1.1.1 github.com/golang-jwt/jwt/v4 v4.4.2 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 @@ -51,6 +52,7 @@ require ( github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f golang.org/x/crypto v0.9.0 golang.org/x/net v0.10.0 + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 golang.org/x/sys v0.8.0 golang.org/x/text v0.9.0 gopkg.in/ini.v1 v1.67.0 @@ -113,7 +115,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/glebarez/go-sqlite v1.21.1 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -141,6 +143,7 @@ require ( github.com/gorilla/mux v1.8.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/h2non/filetype v1.1.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -208,7 +211,7 @@ require ( github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.3 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/theupdateframework/notary v0.7.0 // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect @@ -229,7 +232,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect - go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel v1.15.1 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1 // indirect @@ -237,13 +240,12 @@ require ( go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect go.opentelemetry.io/otel/metric v0.27.0 // indirect go.opentelemetry.io/otel/sdk v1.4.1 // indirect - go.opentelemetry.io/otel/trace v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.15.1 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/image v0.5.0 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/term v0.8.0 // indirect golang.org/x/time v0.1.0 // indirect diff --git a/go.sum b/go.sum index f1c4eae23..7096e0db2 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= @@ -349,6 +349,8 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goh-chunlin/go-onedrive v1.1.1 h1:HGtHk5iG0MZ92zYUtaY04czfZPBIJUr12UuFc+PW8m4= +github.com/goh-chunlin/go-onedrive v1.1.1/go.mod h1:N8qIGHD7tryO734epiBKk5oXcpGwxKET/u3LuBHciTs= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -461,6 +463,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaW github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/h2non/filetype v1.1.1 h1:xvOwnXKAckvtLWsN398qS9QhlxlnVXBjXBydK2/UFB4= +github.com/h2non/filetype v1.1.1/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -827,8 +831,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY= @@ -915,8 +920,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 h1:SLme4Po go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0/go.mod h1:tLYsuf2v8fZreBVwp9gVMhefZlLFZaUiNVSq8QxXRII= go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4= -go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= +go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 h1:imIM3vRDMyZK1ypQlQlO+brE22I9lRhJsBDXpDWjlz8= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 h1:WPpPsAAs8I2rA47v5u0558meKmmwm1Dj99ZbqCV8sZ8= @@ -933,8 +938,8 @@ go.opentelemetry.io/otel/sdk v1.4.1 h1:J7EaW71E0v87qflB4cDolaqq3AcujGrtyIPGQoZOB go.opentelemetry.io/otel/sdk v1.4.1/go.mod h1:NBwHDgDIBYjwK2WNu1OPgsIc2IJzmBXNnvIJxJc8BpE= go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE= go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= +go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.12.0 h1:CMJ/3Wp7iOWES+CYLfnBv+DVmPbB+kmy9PJ92XvlR6c= go.opentelemetry.io/proto/otlp v0.12.0/go.mod h1:TsIjwGWIx5VFYv9KGVlOpxoBl5Dy+63SUguV7GGvlSQ= @@ -1015,6 +1020,7 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=