diff --git a/agent/app/api/v2/app.go b/agent/app/api/v2/app.go index 4f7fb9a23..6b48e7d5a 100644 --- a/agent/app/api/v2/app.go +++ b/agent/app/api/v2/app.go @@ -2,12 +2,12 @@ package v2 import ( "net/http" - "time" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/appicon" "github.com/gin-gonic/gin" ) @@ -210,15 +210,28 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) { helper.BadRequest(c, err) return } - iconBytes, err := appService.GetAppIcon(appKey) + iconBytes, _, etag, err := appService.GetAppIcon(appKey) if err != nil { helper.InternalServer(c, err) return } - c.Header("Content-Type", "image/png") - c.Header("Cache-Control", "public, max-age=31536000, immutable") - c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) - c.Data(http.StatusOK, "image/png", iconBytes) + + if len(iconBytes) == 0 { + c.Status(http.StatusNoContent) + return + } + + c.Header("Cache-Control", "public, max-age=2592000") + + if etag != "" { + c.Header("ETag", etag) + if c.GetHeader("If-None-Match") == etag { + c.Status(http.StatusNotModified) + return + } + } + + c.Data(http.StatusOK, appicon.ContentTypePNG, iconBytes) } // @Tags App diff --git a/agent/app/service/app.go b/agent/app/service/app.go index 65b9f7653..7ab5332c4 100644 --- a/agent/app/service/app.go +++ b/agent/app/service/app.go @@ -25,6 +25,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/appicon" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/files" @@ -46,7 +47,7 @@ type IAppService interface { GetAppUpdate() (*response.AppUpdateRes, error) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) SyncAppListFromLocal(taskID string) - GetAppIcon(key string) ([]byte, error) + GetAppIcon(key string) ([]byte, string, string, error) GetAppDetailByKey(appKey, version string) (response.AppDetailSimpleDTO, error) } @@ -925,26 +926,43 @@ func (a AppService) SyncAppListFromRemote(taskID string) (err error) { if err != nil { return err } - syncTask.AddSubTask(task.GetTaskName(i18n.GetMsgByKey("App"), task.TaskSync, task.TaskScopeAppStore), a.syncAppStoreTask, nil) + + var sharedCtx *appSyncContext + + syncTask.AddSubTask(task.GetTaskName(i18n.GetMsgByKey("App"), task.TaskSync, task.TaskScopeAppStore), a.createSyncAppStoreTask(&sharedCtx), nil) + syncTask.AddSubTask(i18n.GetMsgByKey("SyncAppDetail"), a.createSyncAppStoreMetaTask(&sharedCtx), nil) go func() { if err := syncTask.Execute(); err != nil { _ = NewISettingService().Update("AppStoreLastModified", "0") _ = NewISettingService().Update("AppStoreSyncStatus", constant.StatusError) + return } }() return nil } -func (a AppService) GetAppIcon(key string) ([]byte, error) { +func (a AppService) GetAppIcon(key string) ([]byte, string, string, error) { app, err := appRepo.GetFirst(appRepo.WithKey(key)) if err != nil { - return nil, err + return nil, "", "", err } + + if appicon.IsIconFile(app.Icon) { + fileName, etag := appicon.ParseIconField(app.Icon) + iconBytes, err := appicon.ReadIconFile(fileName) + if err != nil { + global.LOG.Warnf("[AppIcon] read icon file failed key=%s, file=%s, err=%v", key, fileName, err) + return nil, "", "", nil + } + return iconBytes, fileName, etag, nil + } + iconBytes, err := base64.StdEncoding.DecodeString(app.Icon) if err != nil { - return nil, err + global.LOG.Warnf("[AppIcon] decode base64 icon failed key=%s, err=%v", key, err) + return nil, "", "", nil } - return iconBytes, nil + return iconBytes, "", "", nil } diff --git a/agent/app/service/app_sync_task.go b/agent/app/service/app_sync_task.go index 7e584c958..7c547e957 100644 --- a/agent/app/service/app_sync_task.go +++ b/agent/app/service/app_sync_task.go @@ -2,11 +2,11 @@ package service import ( "context" - "encoding/base64" "encoding/json" "fmt" "net/http" "strconv" + "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" @@ -15,6 +15,7 @@ import ( "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/appicon" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/1Panel-dev/1Panel/agent/utils/xpack" @@ -30,92 +31,110 @@ type appSyncContext struct { list *dto.AppList oldAppIds []uint appTags []*model.AppTag + skipMetaSync bool + pendingIcons map[string]string } -func (a AppService) syncAppStoreTask(t *task.Task) (err error) { - updateRes, err := a.GetAppUpdate() - if err != nil { - return err - } - if !updateRes.CanUpdate { - if updateRes.IsSyncing { - t.Log(i18n.GetMsgByKey("AppStoreIsSyncing")) - return nil - } - global.LOG.Infof("[AppStore] Appstore is up to date") - t.Log(i18n.GetMsgByKey("AppStoreIsUpToDate")) - return nil - } +func (a AppService) createSyncAppStoreTask(sharedCtx **appSyncContext) func(t *task.Task) error { + return func(t *task.Task) (err error) { + t.LogStart(i18n.GetMsgByKey("AppStore") + " " + i18n.GetMsgByKey("TaskSync")) - list := &dto.AppList{} - if updateRes.AppList == nil { - list, err = getAppList() + updateRes, err := a.GetAppUpdate() if err != nil { + t.LogFailedWithErr(i18n.GetMsgByKey("CheckAppStoreUpdate"), err) return err } - } else { - list = updateRes.AppList + if !updateRes.CanUpdate { + if updateRes.IsSyncing { + t.Log(i18n.GetMsgByKey("AppStoreIsSyncing")) + return nil + } + global.LOG.Infof("[AppStore] Appstore is up to date") + t.Log(i18n.GetMsgByKey("AppStoreIsUpToDate")) + *sharedCtx = &appSyncContext{skipMetaSync: true} + t.LogSuccess(i18n.GetMsgByKey("AppStore") + " " + i18n.GetMsgByKey("TaskSync")) + return nil + } + + list := &dto.AppList{} + if updateRes.AppList == nil { + list, err = getAppList() + if err != nil { + t.LogFailedWithErr(i18n.GetMsgByKey("DownloadAppList"), err) + return err + } + } else { + list = updateRes.AppList + } + + settingService := NewISettingService() + _ = settingService.Update("AppStoreSyncStatus", constant.StatusSyncing) + + setting, err := settingService.GetSettingInfo() + if err != nil { + t.LogFailedWithErr("GetSettingInfo", err) + return err + } + + ctx := &appSyncContext{ + task: t, + httpClient: http.Client{Timeout: time.Duration(constant.TimeOut20s) * time.Second, Transport: xpack.LoadRequestTransport()}, + baseRemoteUrl: fmt.Sprintf("%s/%s/1panel", global.CONF.RemoteURL.AppRepo, global.CONF.Base.Mode), + systemVersion: setting.SystemVersion, + settingService: settingService, + list: list, + appTags: make([]*model.AppTag, 0), + pendingIcons: make(map[string]string), + } + + if err = SyncTags(list.Extra); err != nil { + t.LogFailedWithErr(i18n.GetMsgByKey("SyncTags"), err) + return err + } + deleteCustomApp() + + oldApps, err := appRepo.GetBy(appRepo.WithNotLocal()) + if err != nil { + t.LogFailedWithErr(i18n.GetMsgByKey("LoadLocalApps"), err) + return err + } + ctx.oldAppIds = make([]uint, 0, len(oldApps)) + for _, old := range oldApps { + ctx.oldAppIds = append(ctx.oldAppIds, old.ID) + } + + ctx.appsMap, ctx.pendingIcons = getApps(oldApps, list.Apps, setting.SystemVersion, t) + + var addCount, updateCount, deleteCount int + if err = ctx.classifyAndPersistAppsWithStats(&addCount, &updateCount, &deleteCount); err != nil { + t.LogFailedWithErr(i18n.GetMsgByKey("PersistApps"), err) + return err + } + + _ = settingService.Update("AppStoreSyncStatus", constant.StatusSyncSuccess) + _ = settingService.Update("AppStoreLastModified", strconv.Itoa(list.LastModified)) + global.LOG.Infof("[AppStore] Appstore sync completed") + + *sharedCtx = ctx + t.Logf("App store sync completed: total=%d, add=%d, update=%d, delete=%d", len(ctx.appsMap), addCount, updateCount, deleteCount) + t.LogSuccess(i18n.GetMsgByKey("AppStore") + " " + i18n.GetMsgByKey("TaskSync")) + return nil } - - settingService := NewISettingService() - _ = settingService.Update("AppStoreSyncStatus", constant.StatusSyncing) - - setting, err := settingService.GetSettingInfo() - if err != nil { - return err - } - - ctx := &appSyncContext{ - task: t, - httpClient: http.Client{Timeout: time.Duration(constant.TimeOut20s) * time.Second, Transport: xpack.LoadRequestTransport()}, - baseRemoteUrl: fmt.Sprintf("%s/%s/1panel", global.CONF.RemoteURL.AppRepo, global.CONF.Base.Mode), - systemVersion: setting.SystemVersion, - settingService: settingService, - list: list, - appTags: make([]*model.AppTag, 0), - } - - if err = SyncTags(list.Extra); err != nil { - return err - } - deleteCustomApp() - - oldApps, err := appRepo.GetBy(appRepo.WithNotLocal()) - if err != nil { - return err - } - ctx.oldAppIds = make([]uint, 0, len(oldApps)) - for _, old := range oldApps { - ctx.oldAppIds = append(ctx.oldAppIds, old.ID) - } - - ctx.appsMap = getApps(oldApps, list.Apps, setting.SystemVersion, t) - - if err = ctx.syncAppIconsAndDetails(); err != nil { - return err - } - - if err = ctx.classifyAndPersistApps(); err != nil { - return err - } - - _ = settingService.Update("AppStoreSyncStatus", constant.StatusSyncSuccess) - _ = settingService.Update("AppStoreLastModified", strconv.Itoa(list.LastModified)) - global.LOG.Infof("[AppStore] Appstore sync completed") - return nil } func (c *appSyncContext) syncAppIconsAndDetails() error { - c.task.LogStart(i18n.GetMsgByKey("SyncAppDetail")) - global.LOG.Infof("[AppStore] sync app detail start, total: %d", len(c.list.Apps)) - - downloadIconNum := 0 total := len(c.list.Apps) + global.LOG.Infof("[AppStore] sync app detail start, total apps: %d", total) - for _, l := range c.list.Apps { - downloadIconNum++ - if downloadIconNum%10 == 0 { - c.task.LogWithProgress(i18n.GetMsgByKey("SyncAppDetail"), downloadIconNum, total) + var ( + icon200Count = 0 + icon304Count = 0 + iconFailCount = 0 + ) + + for i, l := range c.list.Apps { + if (i+1)%10 == 0 { + c.task.LogWithProgress(i18n.GetMsgByKey("SyncAppDetail"), i+1, total) } app, ok := c.appsMap[l.AppProperty.Key] @@ -123,11 +142,20 @@ func (c *appSyncContext) syncAppIconsAndDetails() error { continue } - iconStr := c.downloadAppIcon(l.Icon) - if iconStr == "" { - global.LOG.Infof("[AppStore] save failed url=%s", l.Icon) + iconUrl, hasPending := c.pendingIcons[l.AppProperty.Key] + if hasPending { + status, iconField := c.downloadAppIcon(iconUrl, l.AppProperty.Key, app.Icon) + switch status { + case http.StatusOK: + app.Icon = iconField + icon200Count++ + case http.StatusNotModified: + icon304Count++ + default: + global.LOG.Warnf("[AppStore] download icon failed url=%s, appKey=%s", iconUrl, l.AppProperty.Key) + iconFailCount++ + } } - app.Icon = iconStr app.TagsKey = l.AppProperty.Tags if l.AppProperty.Recommend > 0 { @@ -180,32 +208,113 @@ func (c *appSyncContext) syncAppIconsAndDetails() error { c.appsMap[l.AppProperty.Key] = app } - global.LOG.Infof("[AppStore] download icon success: %d, total: %d", - downloadIconNum, total) + global.LOG.Infof("[AppStore] icon download completed - total: %d, success(200): %d, cached(304): %d, failed: %d", + total, icon200Count, icon304Count, iconFailCount) - c.task.LogSuccess(i18n.GetMsgByKey("SyncAppDetail")) return nil } -func (c *appSyncContext) downloadAppIcon(iconUrl string) string { - iconStr := "" +func (c *appSyncContext) downloadAppIcon(iconUrl, appKey, oldIcon string) (status int, iconField string) { + existingEtag := appicon.GetETagFromIconField(oldIcon) - code, iconRes, err := req_helper.HandleRequestWithClient(&c.httpClient, iconUrl, http.MethodGet, constant.TimeOut20s) - if err == nil { - if code == http.StatusOK { - if len(iconRes) > 0 { - if iconRes[0] != '<' { - iconStr = base64.StdEncoding.EncodeToString(iconRes) - } - } - } else { - global.LOG.Infof("[AppStore] download failed status=%d", code) - } + reqHeaders := make(map[string]string) + if existingEtag != "" { + reqHeaders["If-None-Match"] = existingEtag + } + + resp, err := req_helper.HandleRequestWithHeaders(&c.httpClient, iconUrl, http.MethodGet, constant.TimeOut20s, reqHeaders) + if err != nil { + global.LOG.Warnf("[AppStore] request icon failed url=%s, err=%v", iconUrl, err) + return 0, "" + } + + if resp.StatusCode == http.StatusNotModified { + return http.StatusNotModified, "" + } + + if resp.StatusCode != http.StatusOK { + global.LOG.Warnf("[AppStore] download icon failed url=%s, status=%d", iconUrl, resp.StatusCode) + return 0, "" + } + + if len(resp.Body) == 0 { + global.LOG.Warnf("[AppStore] download icon empty body url=%s", iconUrl) + return 0, "" + } + + if resp.Body[0] == '<' { + global.LOG.Warnf("[AppStore] download icon got HTML response url=%s", iconUrl) + return 0, "" + } + + contentType := resp.Header.Get("Content-Type") + ct := strings.TrimSpace(strings.Split(contentType, ";")[0]) + if strings.ToLower(ct) != "image/png" { + global.LOG.Warnf("[AppStore] unexpected icon content-type: %s, expected image/png, url=%s", ct, iconUrl) + } + + fileName, err := appicon.WriteIconFile(appKey, resp.Body) + if err != nil { + global.LOG.Warnf("[AppStore] write icon file failed appKey=%s, err=%v", appKey, err) + return 0, "" + } + + newEtag := resp.Header.Get("ETag") + iconField = appicon.BuildIconField(fileName, newEtag) + + return http.StatusOK, iconField +} + +func (a AppService) createSyncAppStoreMetaTask(sharedCtx **appSyncContext) func(t *task.Task) error { + return func(t *task.Task) (err error) { + t.LogStart(i18n.GetMsgByKey("SyncAppDetail")) + ctx := *sharedCtx + if ctx == nil { + global.LOG.Warnf("[AppStore] meta sync skipped: shared context is nil") + t.Log(i18n.GetMsgByKey("SyncAppDetail") + " skipped: shared context is nil") + return nil + } + + if ctx.skipMetaSync { + global.LOG.Infof("[AppStore] meta sync skipped: no update needed") + t.Log(i18n.GetMsgByKey("SyncAppDetail") + " skipped: no update needed") + return nil + } + + if ctx.list == nil || ctx.appsMap == nil { + global.LOG.Errorf("[AppStore] meta sync failed: shared context data not initialized") + err := fmt.Errorf("shared context data not initialized") + t.LogFailedWithErr(i18n.GetMsgByKey("SyncAppDetail"), err) + return err + } + + t.Logf("%s: %d apps", i18n.GetMsgByKey("SyncAppDetail"), len(ctx.list.Apps)) + + ctx.task = t + ctx.appTags = make([]*model.AppTag, 0) + + if err = ctx.syncAppIconsAndDetails(); err != nil { + t.LogFailedWithErr(i18n.GetMsgByKey("SyncAppDetail"), err) + return err + } + + if err = ctx.classifyAndPersistApps(); err != nil { + t.LogFailedWithErr(i18n.GetMsgByKey("PersistAppDetails"), err) + return err + } + + global.LOG.Infof("[AppStore] Appstore meta sync completed") + t.LogSuccess(i18n.GetMsgByKey("SyncAppDetail")) + return nil } - return iconStr } func (c *appSyncContext) classifyAndPersistApps() (err error) { + var addCount, updateCount, deleteCount int + return c.classifyAndPersistAppsWithStats(&addCount, &updateCount, &deleteCount) +} + +func (c *appSyncContext) classifyAndPersistAppsWithStats(addCount, updateCount, deleteCount *int) (err error) { tags, _ := tagRepo.All() var ( addAppArray []model.App @@ -233,6 +342,10 @@ func (c *appSyncContext) classifyAndPersistApps() (err error) { } } + *addCount = len(addAppArray) + *updateCount = len(updateAppArray) + *deleteCount = len(deleteAppArray) + tx, ctx := getTxAndContext() defer func() { if err != nil { @@ -260,9 +373,11 @@ func (c *appSyncContext) classifyAndPersistApps() (err error) { tagMap[tag.Key] = tag.ID } - for _, update := range updateAppArray { - if err = appRepo.Save(ctx, &update); err != nil { - return + if len(updateAppArray) > 0 { + for _, update := range updateAppArray { + if err = appRepo.Save(ctx, &update); err != nil { + return + } } } @@ -274,6 +389,7 @@ func (c *appSyncContext) classifyAndPersistApps() (err error) { deleteDetails []model.AppDetail ) + totalDetails := 0 for _, app := range apps { for _, tag := range app.TagsKey { tagId, ok := tagMap[tag] @@ -289,6 +405,7 @@ func (c *appSyncContext) classifyAndPersistApps() (err error) { } for _, d := range app.Details { + totalDetails++ d.AppId = app.ID if d.ID == 0 { addDetails = append(addDetails, d) @@ -324,9 +441,11 @@ func (c *appSyncContext) classifyAndPersistApps() (err error) { } } - for _, u := range updateDetails { - if err = appDetailRepo.Update(ctx, u); err != nil { - return + if len(updateDetails) > 0 { + for _, u := range updateDetails { + if err = appDetailRepo.Update(ctx, u); err != nil { + return + } } } diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index 90fd6510a..03d76d3de 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -1295,8 +1295,9 @@ func getAppDetails(details []model.AppDetail, versions []dto.AppConfigVersion) m return appDetails } -func getApps(oldApps []model.App, items []dto.AppDefine, systemVersion string, task *task.Task) map[string]model.App { +func getApps(oldApps []model.App, items []dto.AppDefine, systemVersion string, task *task.Task) (map[string]model.App, map[string]string) { apps := make(map[string]model.App, len(oldApps)) + pendingIcons := make(map[string]string, len(items)) for _, old := range oldApps { old.Status = constant.AppTakeDown apps[old.Key] = old @@ -1337,9 +1338,12 @@ func getApps(oldApps []model.App, items []dto.AppDefine, systemVersion string, t app.Architectures = strings.Join(config.Architectures, ",") app.GpuSupport = config.GpuSupport app.BatchInstallSupport = config.BatchInstallSupport + if item.Icon != "" { + pendingIcons[key] = item.Icon + } apps[key] = app } - return apps + return apps, pendingIcons } func handleLocalAppDetail(versionDir string, appDetail *model.AppDetail) error { diff --git a/agent/global/config.go b/agent/global/config.go index 27fbe4c34..31bcad6b5 100644 --- a/agent/global/config.go +++ b/agent/global/config.go @@ -34,6 +34,7 @@ type SystemDir struct { AppDir string ResourceDir string + IconCacheDir string AppResourceDir string AppInstallDir string LocalAppResourceDir string diff --git a/agent/init/dir/dir.go b/agent/init/dir/dir.go index 651f78601..042d98863 100644 --- a/agent/init/dir/dir.go +++ b/agent/init/dir/dir.go @@ -21,6 +21,7 @@ func Init() { global.Dir.AppDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/apps")) global.Dir.ResourceDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/resource")) + global.Dir.IconCacheDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/resource/icon")) global.Dir.AppResourceDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/resource/apps")) global.Dir.AppInstallDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/apps")) global.Dir.LocalAppResourceDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/resource/apps/local")) diff --git a/agent/utils/appicon/appicon.go b/agent/utils/appicon/appicon.go new file mode 100644 index 000000000..f276dd8c1 --- /dev/null +++ b/agent/utils/appicon/appicon.go @@ -0,0 +1,93 @@ +package appicon + +import ( + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/1Panel-dev/1Panel/agent/global" +) + +const IconPrefix = "app_" + +func IsIconFile(icon string) bool { + return strings.HasPrefix(icon, IconPrefix) +} + +func ParseIconField(icon string) (fileName, etag string) { + if !IsIconFile(icon) { + return "", "" + } + parts := strings.SplitN(icon, "?", 2) + fileName = parts[0] + if len(parts) == 2 { + values, err := url.ParseQuery(parts[1]) + if err == nil { + etag = values.Get("etag") + } + } + return +} + +func BuildIconField(fileName, etag string) string { + if etag == "" { + return fileName + } + return fmt.Sprintf("%s?etag=%s", fileName, url.QueryEscape(etag)) +} + +func GetIconFilePath(fileName string) string { + return path.Join(global.Dir.IconCacheDir, fileName) +} + +func BuildIconFileName(appKey, ext string) string { + return fmt.Sprintf("%s%s.%s", IconPrefix, appKey, ext) +} + +const ContentTypePNG = "image/png" + +func WriteIconFile(appKey string, data []byte) (fileName string, err error) { + fileName = BuildIconFileName(appKey, "png") + filePath := GetIconFilePath(fileName) + + _ = CleanOldIconFiles(appKey) + + err = os.WriteFile(filePath, data, 0644) + return +} + +func CleanOldIconFiles(appKey string) error { + pattern := path.Join(global.Dir.IconCacheDir, fmt.Sprintf("%s%s.*", IconPrefix, appKey)) + matches, err := filepath.Glob(pattern) + if err != nil { + return err + } + keepFileName := BuildIconFileName(appKey, "png") + for _, match := range matches { + baseName := filepath.Base(match) + if baseName == keepFileName { + continue + } + _ = os.Remove(match) + } + return nil +} + +func ReadIconFile(fileName string) ([]byte, error) { + filePath := GetIconFilePath(fileName) + return os.ReadFile(filePath) +} + +func IconFileExists(fileName string) bool { + filePath := GetIconFilePath(fileName) + _, err := os.Stat(filePath) + return err == nil +} + +func GetETagFromIconField(icon string) string { + _, etag := ParseIconField(icon) + return etag +} diff --git a/agent/utils/req_helper/request.go b/agent/utils/req_helper/request.go index 60f660651..828c94678 100644 --- a/agent/utils/req_helper/request.go +++ b/agent/utils/req_helper/request.go @@ -83,6 +83,46 @@ func HandleRequestWithClient(client *http.Client, url, method string, timeout in return resp.StatusCode, body, nil } +type RequestResponse struct { + StatusCode int + Body []byte + Header http.Header +} + +func HandleRequestWithHeaders(client *http.Client, url, method string, timeout int, reqHeaders map[string]string) (*RequestResponse, error) { + defer func() { + if r := recover(); r != nil { + global.LOG.Errorf("handle request failed, error message: %v", r) + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + request, err := http.NewRequestWithContext(ctx, method, url, nil) + if err != nil { + return nil, err + } + for k, v := range reqHeaders { + request.Header.Set(k, v) + } + resp, err := client.Do(request) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return &RequestResponse{ + StatusCode: resp.StatusCode, + Body: body, + Header: resp.Header, + }, nil +} + func RequestFile(url, method string, timeout int) (io.ReadCloser, context.CancelFunc, error) { defer func() { if r := recover(); r != nil {