529 lines
17 KiB
Go
529 lines
17 KiB
Go
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"`
|
|
}
|
|
|
|
type desktopClientReleaseDirectUploadInitRequest 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"`
|
|
SHA256 string `json:"sha256" binding:"required"`
|
|
ContentMD5 string `json:"content_md5"`
|
|
}
|
|
|
|
type desktopClientReleaseDirectUploadCompleteRequest 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"`
|
|
OSSObjectKey string `json:"oss_object_key" binding:"required"`
|
|
FileName string `json:"file_name" binding:"required"`
|
|
FileSizeBytes int64 `json:"file_size_bytes" binding:"required"`
|
|
SHA256 string `json:"sha256" binding:"required"`
|
|
}
|
|
|
|
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 initiateDesktopClientReleaseDirectUploadHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var body desktopClientReleaseDirectUploadInitRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
result, err := svc.InitiateDirectUpload(c.Request.Context(), app.DesktopClientReleaseDirectUploadInitInput{
|
|
Platform: body.Platform,
|
|
Arch: body.Arch,
|
|
Channel: body.Channel,
|
|
Version: body.Version,
|
|
Kind: body.Kind,
|
|
FileName: body.FileName,
|
|
FileSizeBytes: body.FileSizeBytes,
|
|
SHA256: body.SHA256,
|
|
ContentMD5: body.ContentMD5,
|
|
})
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, result)
|
|
}
|
|
}
|
|
|
|
func completeDesktopClientReleaseDirectUploadHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var body desktopClientReleaseDirectUploadCompleteRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
result, err := svc.CompleteDirectUpload(c.Request.Context(), app.DesktopClientReleaseDirectUploadCompleteInput{
|
|
Platform: body.Platform,
|
|
Arch: body.Arch,
|
|
Channel: body.Channel,
|
|
Version: body.Version,
|
|
Kind: body.Kind,
|
|
OSSObjectKey: body.OSSObjectKey,
|
|
FileName: body.FileName,
|
|
FileSizeBytes: body.FileSizeBytes,
|
|
SHA256: body.SHA256,
|
|
})
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, result)
|
|
}
|
|
}
|
|
|
|
func startDesktopClientReleaseDirectUploadCompleteJobHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var body desktopClientReleaseDirectUploadCompleteRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
result := svc.StartDirectUploadCompleteJob(app.DesktopClientReleaseDirectUploadCompleteInput{
|
|
Platform: body.Platform,
|
|
Arch: body.Arch,
|
|
Channel: body.Channel,
|
|
Version: body.Version,
|
|
Kind: body.Kind,
|
|
OSSObjectKey: body.OSSObjectKey,
|
|
FileName: body.FileName,
|
|
FileSizeBytes: body.FileSizeBytes,
|
|
SHA256: body.SHA256,
|
|
})
|
|
response.Success(c, result)
|
|
}
|
|
}
|
|
|
|
func getDesktopClientReleaseCompleteJobHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
result, err := svc.GetCompleteJob(c.Param("job_id"))
|
|
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 startDesktopClientReleaseUploadCompleteJobHandler(svc *app.DesktopClientReleaseService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
result, err := svc.StartChunkedUploadCompleteJob(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)
|
|
}
|