feat(desktop): add client release updater
Desktop Client Build / Resolve Build Metadata (push) Successful in 19s
Frontend CI / Frontend (push) Successful in 3m20s
Backend CI / Backend (push) Successful in 16m48s
Desktop Client Build / Build Desktop Client (push) Successful in 24m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 37s
Desktop Client Build / Resolve Build Metadata (push) Successful in 19s
Frontend CI / Frontend (push) Successful in 3m20s
Backend CI / Backend (push) Successful in 16m48s
Desktop Client Build / Build Desktop Client (push) Successful in 24m46s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 37s
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/ops/app"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
)
|
||||
|
||||
type desktopClientReleaseRequest struct {
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
Arch string `json:"arch"`
|
||||
Channel string `json:"channel"`
|
||||
Version string `json:"version" binding:"required"`
|
||||
MinSupportedVersion *string `json:"min_supported_version"`
|
||||
DownloadSource string `json:"download_source"`
|
||||
OSSObjectKey *string `json:"oss_object_key"`
|
||||
CustomDownloadURL *string `json:"custom_download_url"`
|
||||
FileName *string `json:"file_name"`
|
||||
FileSizeBytes *int64 `json:"file_size_bytes"`
|
||||
SHA256 *string `json:"sha256"`
|
||||
UpdaterDownloadSource *string `json:"updater_download_source"`
|
||||
UpdaterOSSObjectKey *string `json:"updater_oss_object_key"`
|
||||
UpdaterCustomDownloadURL *string `json:"updater_custom_download_url"`
|
||||
UpdaterFileName *string `json:"updater_file_name"`
|
||||
UpdaterFileSizeBytes *int64 `json:"updater_file_size_bytes"`
|
||||
UpdaterSHA256 *string `json:"updater_sha256"`
|
||||
ReleaseNotes *string `json:"release_notes"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type setDesktopClientReleaseEnabledRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func listDesktopClientReleasesHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
result, err := svc.List(c.Request.Context(), app.DesktopClientReleaseListInput{
|
||||
Keyword: c.Query("keyword"),
|
||||
Platform: c.Query("platform"),
|
||||
Channel: c.Query("channel"),
|
||||
Enabled: parseOptionalBoolQuery(c.Query("enabled")),
|
||||
Page: page,
|
||||
Size: size,
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func createDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body desktopClientReleaseRequest
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := svc.Create(c.Request.Context(), actorFromGin(c), body.toInput())
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func updateDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := parseIDParam(c)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
var body desktopClientReleaseRequest
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := svc.Update(c.Request.Context(), actorFromGin(c), id, body.toInput())
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func setDesktopClientReleaseEnabledHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := parseIDParam(c)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
var body setDesktopClientReleaseEnabledRequest
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := svc.SetEnabled(c.Request.Context(), actorFromGin(c), id, body.Enabled)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := parseIDParam(c)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
if err := svc.Delete(c.Request.Context(), actorFromGin(c), id); err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, gin.H{"id": id})
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDesktopClientReleaseDownloadURLHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, err := parseIDParam(c)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
result, err := svc.ResolveDownloadURLByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func uploadDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40071, "release_file_required", "请选择要上传的客户端安装包"))
|
||||
return
|
||||
}
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40071, "release_file_open_failed", "客户端安装包读取失败"))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40071, "release_file_read_failed", "客户端安装包读取失败"))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := svc.UploadOSS(c.Request.Context(), app.DesktopClientReleaseUploadInput{
|
||||
Platform: c.PostForm("platform"),
|
||||
Arch: c.PostForm("arch"),
|
||||
Channel: c.PostForm("channel"),
|
||||
Version: c.PostForm("version"),
|
||||
Kind: c.DefaultPostForm("kind", app.DesktopClientAssetKindInstaller),
|
||||
FileName: fileHeader.Filename,
|
||||
Content: content,
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func checkDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
result, err := svc.CheckLatest(c.Request.Context(), app.DesktopClientReleaseCheckInput{
|
||||
Platform: c.Query("platform"),
|
||||
Arch: c.Query("arch"),
|
||||
Channel: c.DefaultQuery("channel", "stable"),
|
||||
CurrentVersion: c.Query("version"),
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveLatestDesktopClientReleaseDownloadURLHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
result, err := svc.ResolveLatestDownloadURL(c.Request.Context(), app.DesktopClientReleaseCheckInput{
|
||||
Platform: c.Query("platform"),
|
||||
Arch: c.Query("arch"),
|
||||
Channel: c.DefaultQuery("channel", "stable"),
|
||||
CurrentVersion: c.Query("version"),
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, result)
|
||||
}
|
||||
}
|
||||
|
||||
func desktopClientReleaseUpdaterFeedHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
feed, err := svc.ResolveLatestUpdaterFeed(c.Request.Context(), app.DesktopClientReleaseCheckInput{
|
||||
Platform: firstNonEmpty(c.Param("platform"), c.Query("platform")),
|
||||
Arch: firstNonEmpty(c.Param("arch"), c.Query("arch")),
|
||||
Channel: firstNonEmpty(c.Param("channel"), c.DefaultQuery("channel", "stable")),
|
||||
CurrentVersion: firstNonEmpty(c.Param("version"), c.Query("version")),
|
||||
}, c.Param("feed"))
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/x-yaml; charset=utf-8")
|
||||
c.Header("Cache-Control", "no-store")
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, sanitizeHeaderFileName(feed.FileName)))
|
||||
c.Data(http.StatusOK, "application/x-yaml; charset=utf-8", feed.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func (r desktopClientReleaseRequest) toInput() app.DesktopClientReleaseInput {
|
||||
enabled := true
|
||||
if r.Enabled != nil {
|
||||
enabled = *r.Enabled
|
||||
}
|
||||
return app.DesktopClientReleaseInput{
|
||||
Platform: r.Platform,
|
||||
Arch: r.Arch,
|
||||
Channel: r.Channel,
|
||||
Version: r.Version,
|
||||
MinSupportedVersion: r.MinSupportedVersion,
|
||||
DownloadSource: r.DownloadSource,
|
||||
OSSObjectKey: r.OSSObjectKey,
|
||||
CustomDownloadURL: r.CustomDownloadURL,
|
||||
FileName: r.FileName,
|
||||
FileSizeBytes: r.FileSizeBytes,
|
||||
SHA256: r.SHA256,
|
||||
UpdaterDownloadSource: r.UpdaterDownloadSource,
|
||||
UpdaterOSSObjectKey: r.UpdaterOSSObjectKey,
|
||||
UpdaterCustomDownloadURL: r.UpdaterCustomDownloadURL,
|
||||
UpdaterFileName: r.UpdaterFileName,
|
||||
UpdaterFileSizeBytes: r.UpdaterFileSizeBytes,
|
||||
UpdaterSHA256: r.UpdaterSHA256,
|
||||
ReleaseNotes: r.ReleaseNotes,
|
||||
ForceUpdate: r.ForceUpdate,
|
||||
Enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sanitizeHeaderFileName(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "latest.yml"
|
||||
}
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r == '"' || r == '\\' || r == '\r' || r == '\n' {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, value)
|
||||
}
|
||||
Reference in New Issue
Block a user