feat: add chunked desktop client release uploads
Deployment Config CI / Deployment Config (push) Successful in 28s
Frontend CI / Frontend (push) Successful in 2m12s
Backend CI / Backend (push) Successful in 14m44s

This commit is contained in:
2026-06-01 01:31:54 +08:00
parent c5da0cf507
commit 76df672133
14 changed files with 1195 additions and 57 deletions
@@ -2,7 +2,6 @@ package transport
import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
@@ -40,6 +39,17 @@ type setDesktopClientReleaseEnabledRequest struct {
Enabled bool `json:"enabled"`
}
type desktopClientReleaseUploadInitRequest struct {
Platform string `json:"platform" binding:"required"`
Arch string `json:"arch"`
Channel string `json:"channel"`
Version string `json:"version" binding:"required"`
Kind string `json:"kind"`
FileName string `json:"file_name" binding:"required"`
FileSizeBytes int64 `json:"file_size_bytes" binding:"required"`
ChunkSizeBytes int64 `json:"chunk_size_bytes"`
}
func listDesktopClientReleasesHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
return func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
@@ -151,11 +161,28 @@ func resolveDesktopClientReleaseDownloadURLHandler(svc *app.DesktopClientRelease
func uploadDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.ContentLength > app.MaxDesktopClientReleaseMultipartBytes() {
response.Error(c, response.ErrBadRequest(40072, "release_file_too_large", "客户端安装包不能超过 1GB"))
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, app.MaxDesktopClientReleaseMultipartBytes())
fileHeader, err := c.FormFile("file")
if err != nil {
if strings.Contains(err.Error(), "request body too large") {
response.Error(c, response.ErrBadRequest(40072, "release_file_too_large", "客户端安装包不能超过 1GB"))
return
}
response.Error(c, response.ErrBadRequest(40071, "release_file_required", "请选择要上传的客户端安装包"))
return
}
if c.Request.MultipartForm != nil {
defer c.Request.MultipartForm.RemoveAll()
}
if fileHeader.Size > app.MaxDesktopClientReleaseUploadBytes {
response.Error(c, response.ErrBadRequest(40072, "release_file_too_large", "客户端安装包不能超过 1GB"))
return
}
file, err := fileHeader.Open()
if err != nil {
response.Error(c, response.ErrBadRequest(40071, "release_file_open_failed", "客户端安装包读取失败"))
@@ -163,20 +190,15 @@ func uploadDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin
}
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,
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,
ContentReader: file,
ContentSizeBytes: fileHeader.Size,
})
if err != nil {
response.Error(c, err)
@@ -186,6 +208,102 @@ func uploadDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin
}
}
func initiateDesktopClientReleaseUploadHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
return func(c *gin.Context) {
var body desktopClientReleaseUploadInitRequest
if err := c.ShouldBindJSON(&body); err != nil {
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
return
}
result, err := svc.InitiateChunkedUpload(c.Request.Context(), app.DesktopClientReleaseChunkedUploadInitInput{
Platform: body.Platform,
Arch: body.Arch,
Channel: body.Channel,
Version: body.Version,
Kind: body.Kind,
FileName: body.FileName,
FileSizeBytes: body.FileSizeBytes,
ChunkSizeBytes: body.ChunkSizeBytes,
})
if err != nil {
response.Error(c, err)
return
}
response.Success(c, result)
}
}
func uploadDesktopClientReleaseChunkHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.ContentLength > app.MaxDesktopClientReleaseChunkMultipartBytes() {
response.Error(c, response.ErrBadRequest(40076, "invalid_release_upload_chunk", "上传分片不能超过 64MB"))
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, app.MaxDesktopClientReleaseChunkMultipartBytes())
chunkIndex, err := strconv.Atoi(c.Param("chunk_index"))
if err != nil {
response.Error(c, response.ErrBadRequest(40076, "invalid_release_upload_chunk", "上传分片序号无效"))
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
if strings.Contains(err.Error(), "request body too large") {
response.Error(c, response.ErrBadRequest(40076, "invalid_release_upload_chunk", "上传分片不能超过 64MB"))
return
}
response.Error(c, response.ErrBadRequest(40071, "release_file_required", "请选择要上传的客户端安装包"))
return
}
if c.Request.MultipartForm != nil {
defer c.Request.MultipartForm.RemoveAll()
}
if fileHeader.Size > app.MaxDesktopClientReleaseChunkSizeBytes {
response.Error(c, response.ErrBadRequest(40076, "invalid_release_upload_chunk", "上传分片不能超过 64MB"))
return
}
file, err := fileHeader.Open()
if err != nil {
response.Error(c, response.ErrBadRequest(40071, "release_file_open_failed", "客户端安装包读取失败"))
return
}
defer file.Close()
result, err := svc.UploadChunk(c.Request.Context(), app.DesktopClientReleaseChunkUploadInput{
UploadID: c.Param("upload_id"),
ChunkIndex: chunkIndex,
ContentReader: file,
ContentSizeBytes: fileHeader.Size,
})
if err != nil {
response.Error(c, err)
return
}
response.Success(c, result)
}
}
func completeDesktopClientReleaseUploadHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
return func(c *gin.Context) {
result, err := svc.CompleteChunkedUpload(c.Request.Context(), c.Param("upload_id"))
if err != nil {
response.Error(c, err)
return
}
response.Success(c, result)
}
}
func abortDesktopClientReleaseUploadHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
return func(c *gin.Context) {
if err := svc.AbortChunkedUpload(c.Param("upload_id")); err != nil {
response.Error(c, err)
return
}
response.Success(c, gin.H{"upload_id": c.Param("upload_id")})
}
}
func checkDesktopClientReleaseHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
return func(c *gin.Context) {
result, err := svc.CheckLatest(c.Request.Context(), app.DesktopClientReleaseCheckInput{
+4
View File
@@ -173,6 +173,10 @@ func RegisterRoutes(d Deps) {
authed.GET("/desktop-client/releases", listDesktopClientReleasesHandler(d.Releases))
authed.POST("/desktop-client/releases/upload", uploadDesktopClientReleaseHandler(d.Releases))
authed.POST("/desktop-client/release-uploads", initiateDesktopClientReleaseUploadHandler(d.Releases))
authed.POST("/desktop-client/release-uploads/:upload_id/chunks/:chunk_index", uploadDesktopClientReleaseChunkHandler(d.Releases))
authed.POST("/desktop-client/release-uploads/:upload_id/complete", completeDesktopClientReleaseUploadHandler(d.Releases))
authed.DELETE("/desktop-client/release-uploads/:upload_id", abortDesktopClientReleaseUploadHandler(d.Releases))
authed.POST("/desktop-client/releases", createDesktopClientReleaseHandler(d.Releases))
authed.PATCH("/desktop-client/releases/:id", updateDesktopClientReleaseHandler(d.Releases))
authed.GET("/desktop-client/releases/:id/download-url", resolveDesktopClientReleaseDownloadURLHandler(d.Releases))