mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
feat: implement app icon management and caching mechanism (#11719)
* feat: implement app icon management and caching mechanism * feat: enhance app synchronization and icon management - Refactored app synchronization tasks to improve structure and clarity. - Introduced shared context for managing app sync state and metadata. - Updated icon handling to ensure proper content type and caching. - Adjusted cache control settings for app icons to extend cache duration. - Improved error handling and logging during app sync processes. * refactor: streamline app icon retrieval by removing unused fileName return - Removed the fileName return value from GetAppIcon function as it was not utilized. - Enhanced the GetAppIcon method in BaseApi to improve clarity and maintainability. - Ensured proper caching headers are set for app icons.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,6 +34,7 @@ type SystemDir struct {
|
||||
|
||||
AppDir string
|
||||
ResourceDir string
|
||||
IconCacheDir string
|
||||
AppResourceDir string
|
||||
AppInstallDir string
|
||||
LocalAppResourceDir string
|
||||
|
||||
@@ -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"))
|
||||
|
||||
93
agent/utils/appicon/appicon.go
Normal file
93
agent/utils/appicon/appicon.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user