package transport import ( "fmt" "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"` } 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")) 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) { 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", "客户端安装包读取失败")) return } defer file.Close() 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, ContentReader: file, ContentSizeBytes: fileHeader.Size, }) if err != nil { response.Error(c, err) return } response.Success(c, result) } } 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{ 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) }