mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-30 16:05:30 +00:00
perf(appstore): reduce resource requests during synchronization
This commit is contained in:
@@ -207,6 +207,7 @@ func (b *BaseApi) GetAppListUpdate(c *gin.Context) {
|
||||
// @Accept json
|
||||
// @Param key path string true "app key"
|
||||
// @Success 200 {file} file "app icon"
|
||||
// @Success 302 "redirect to the official icon URL"
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /apps/icon/:key [get]
|
||||
@@ -216,12 +217,18 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
iconBytes, filename, etag, err := appService.GetAppIcon(appKey)
|
||||
iconBytes, source, etag, err := appService.GetAppIcon(appKey)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if appicon.IsRemoteURL(source) {
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Redirect(http.StatusFound, source)
|
||||
return
|
||||
}
|
||||
|
||||
if len(iconBytes) == 0 {
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
@@ -229,7 +236,7 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) {
|
||||
|
||||
c.Header("Cache-Control", "public, max-age=2592000")
|
||||
|
||||
if etag != "" && filename != "" {
|
||||
if etag != "" && source != "" {
|
||||
c.Header("ETag", etag)
|
||||
if c.GetHeader("If-None-Match") == etag {
|
||||
c.Status(http.StatusNotModified)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/appicon"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func TestAppIconEndpointPreservesRemoteAndHistoricalSources(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "icons.db")), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.AppTag{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldDB, oldDir, oldLog := global.DB, global.Dir, global.LOG
|
||||
global.DB = db
|
||||
global.Dir.IconCacheDir = t.TempDir()
|
||||
global.LOG = logrus.New()
|
||||
global.LOG.SetOutput(io.Discard)
|
||||
t.Cleanup(func() {
|
||||
global.DB, global.Dir, global.LOG = oldDB, oldDir, oldLog
|
||||
sqlDB, _ := db.DB()
|
||||
_ = sqlDB.Close()
|
||||
})
|
||||
|
||||
var imageRequests atomic.Int32
|
||||
cdn := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
imageRequests.Add(1)
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
t.Cleanup(cdn.Close)
|
||||
remoteURL := cdn.URL + "/icon.png?token=a%2Bb&v=42#image"
|
||||
image := "\x89PNG\r\n\x1a\noriginal-icon"
|
||||
fileName := "app_php.png"
|
||||
if err := os.WriteFile(filepath.Join(global.Dir.IconCacheDir, fileName), []byte(image), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app := model.App{Key: "php", Resource: constant.AppResourceRemote}
|
||||
if err := db.Create(&app).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
router := gin.New()
|
||||
router.GET("/api/v2/apps/icon/:key", (&BaseApi{}).GetAppIcon)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
icon string
|
||||
ifNoneMatch string
|
||||
status int
|
||||
body string
|
||||
etag string
|
||||
}{
|
||||
{"remote URL", remoteURL, "", http.StatusFound, "", ""},
|
||||
{"remote URL ignores legacy etag", remoteURL, `"old"`, http.StatusFound, "", ""},
|
||||
{"historical file", appicon.BuildIconField(fileName, `"42"`), "", http.StatusOK, image, `"42"`},
|
||||
{"historical file cached", appicon.BuildIconField(fileName, `"42"`), `"42"`, http.StatusNotModified, "", `"42"`},
|
||||
{"historical file changed", appicon.BuildIconField(fileName, `"42"`), `"old"`, http.StatusOK, image, `"42"`},
|
||||
{"historical base64", base64.StdEncoding.EncodeToString([]byte(image)), "", http.StatusOK, image, ""},
|
||||
{"missing file", "app_missing.png?etag=old", "", http.StatusNoContent, "", ""},
|
||||
{"invalid base64", "not-an-icon", "", http.StatusNoContent, "", ""},
|
||||
{"invalid URL", "javascript:alert(1)", "", http.StatusNoContent, "", ""},
|
||||
{"empty icon", "", "", http.StatusNoContent, "", ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := db.Model(&app).UpdateColumn("icon", tc.icon).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v2/apps/icon/php?operateNode=child", nil)
|
||||
req.Header.Set("If-None-Match", tc.ifNoneMatch)
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
if res.Code != tc.status {
|
||||
t.Fatalf("status = %d, want %d; body = %s", res.Code, tc.status, res.Body.String())
|
||||
}
|
||||
if res.Header().Get("ETag") != tc.etag {
|
||||
t.Fatalf("etag = %q, want %q", res.Header().Get("ETag"), tc.etag)
|
||||
}
|
||||
if tc.status == http.StatusFound {
|
||||
if res.Header().Get("Location") != remoteURL || res.Header().Get("Cache-Control") != "no-cache" {
|
||||
t.Fatalf("redirect URL or cache policy was changed: %v", res.Header())
|
||||
}
|
||||
} else if res.Body.String() != tc.body {
|
||||
t.Fatalf("body = %q, want %q", res.Body.String(), tc.body)
|
||||
}
|
||||
if imageRequests.Load() != 0 {
|
||||
t.Fatalf("icon endpoint fetched the image %d times", imageRequests.Load())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -203,6 +203,7 @@ func (b *BaseApi) GetServices(c *gin.Context) {
|
||||
// @Tags App
|
||||
// @Summary Search app update version by install id
|
||||
// @Accept json
|
||||
// @Param request body request.AppUpdateVersion true "request"
|
||||
// @Success 200 {array} dto.AppVersion
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
|
||||
Reference in New Issue
Block a user