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 f3b3ffcf0..32cd9a78e 100644 Binary files a/frontend/src/assets/iconfont/iconfont.ttf and b/frontend/src/assets/iconfont/iconfont.ttf differ diff --git a/frontend/src/assets/iconfont/iconfont.woff b/frontend/src/assets/iconfont/iconfont.woff index 9e8513443..40a4e87d0 100644 Binary files a/frontend/src/assets/iconfont/iconfont.woff and b/frontend/src/assets/iconfont/iconfont.woff differ diff --git a/frontend/src/assets/iconfont/iconfont.woff2 b/frontend/src/assets/iconfont/iconfont.woff2 index bb96c94f4..a4de294df 100644 Binary files a/frontend/src/assets/iconfont/iconfont.woff2 and b/frontend/src/assets/iconfont/iconfont.woff2 differ diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index bcff6b0a8..c754dcad9 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -989,6 +989,11 @@ const message = { S3: 'Amazon S3', MINIO: 'MINIO', SFTP: 'SFTP', + OneDrive: 'Microsoft OneDrive', + backupDir: 'Backup dir', + isCN: 'Domestic version', + code: 'Auth code', + loadCode: 'Acquire', COS: 'Tencent COS', KODO: 'Qiniu Kodo', domainHelper: 'The accelerated domain name must contain http:// or https://', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 10f67e2a0..e0b25b30d 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -967,6 +967,11 @@ const message = { S3: '亚马逊 S3 云存储', MINIO: 'MINIO', SFTP: 'SFTP', + OneDrive: '微软 OneDrive', + backupDir: '备份路径', + isCN: '国内版', + code: '授权码', + loadCode: '获取', COS: '腾讯云 COS', KODO: '七牛云 Kodo', domainHelper: '加速域名必须包含 http:// 或者 https://', diff --git a/frontend/src/views/setting/backup-account/index.vue b/frontend/src/views/setting/backup-account/index.vue index 4911f71b4..296db77f3 100644 --- a/frontend/src/views/setting/backup-account/index.vue +++ b/frontend/src/views/setting/backup-account/index.vue @@ -66,6 +66,9 @@ {{ 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=