179 lines
5.0 KiB
Go
179 lines
5.0 KiB
Go
|
|
package service
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"crypto/sha256"
|
|||
|
|
"encoding/hex"
|
|||
|
|
"io"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"regexp"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
|
|||
|
|
"nl-pms-api/internal/commonservice"
|
|||
|
|
"nl-pms-api/internal/config"
|
|||
|
|
"nl-pms-api/internal/model"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var versionRe = regexp.MustCompile(`^\d+\.\d+\.\d+([-.][A-Za-z0-9.]+)?$`)
|
|||
|
|
|
|||
|
|
// ReleaseService 客户端发版。
|
|||
|
|
type ReleaseService struct {
|
|||
|
|
DB *gorm.DB
|
|||
|
|
Cfg *config.Config
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type LatestRelease struct {
|
|||
|
|
Version string `json:"version"`
|
|||
|
|
Channel string `json:"channel"`
|
|||
|
|
SHA256 string `json:"sha256"`
|
|||
|
|
SizeBytes int64 `json:"sizeBytes"`
|
|||
|
|
Changelog string `json:"changelog"`
|
|||
|
|
CreatedAt string `json:"createdAt"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// List 发版列表(新→旧)。
|
|||
|
|
func (s *ReleaseService) List(channel string) ([]model.AppRelease, error) {
|
|||
|
|
channel = normalizeChannel(channel)
|
|||
|
|
var rows []model.AppRelease
|
|||
|
|
q := s.DB.Order("id DESC").Limit(50)
|
|||
|
|
if channel != "" {
|
|||
|
|
q = q.Where("channel = ?", channel)
|
|||
|
|
}
|
|||
|
|
if err := q.Find(&rows).Error; err != nil {
|
|||
|
|
return nil, commonservice.Internal("QUERY_FAILED")
|
|||
|
|
}
|
|||
|
|
if rows == nil {
|
|||
|
|
rows = []model.AppRelease{}
|
|||
|
|
}
|
|||
|
|
return rows, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Latest 当前最新版元数据。
|
|||
|
|
func (s *ReleaseService) Latest(channel string) (*LatestRelease, error) {
|
|||
|
|
channel = normalizeChannel(channel)
|
|||
|
|
var row model.AppRelease
|
|||
|
|
err := s.DB.Where("channel = ? AND is_latest = 1", channel).Order("id DESC").First(&row).Error
|
|||
|
|
if err == gorm.ErrRecordNotFound {
|
|||
|
|
return nil, commonservice.NotFound("NO_RELEASE")
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, commonservice.Internal("QUERY_FAILED")
|
|||
|
|
}
|
|||
|
|
return &LatestRelease{
|
|||
|
|
Version: row.Version,
|
|||
|
|
Channel: row.Channel,
|
|||
|
|
SHA256: row.SHA256,
|
|||
|
|
SizeBytes: row.SizeBytes,
|
|||
|
|
Changelog: row.Changelog,
|
|||
|
|
CreatedAt: row.CreatedAt,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Upload 保存安装包并写入元数据(默认不标 latest,需 Publish)。
|
|||
|
|
func (s *ReleaseService) Upload(version, channel, changelog string, r io.Reader, size int64) (*model.AppRelease, error) {
|
|||
|
|
version = strings.TrimSpace(version)
|
|||
|
|
channel = normalizeChannel(channel)
|
|||
|
|
changelog = strings.TrimSpace(changelog)
|
|||
|
|
if !versionRe.MatchString(version) {
|
|||
|
|
return nil, commonservice.BadRequest("VERSION_INVALID")
|
|||
|
|
}
|
|||
|
|
if size <= 0 {
|
|||
|
|
return nil, commonservice.BadRequest("EMPTY_FILE")
|
|||
|
|
}
|
|||
|
|
if size > s.Cfg.MaxReleaseBytes() {
|
|||
|
|
return nil, commonservice.BadRequest("FILE_TOO_LARGE")
|
|||
|
|
}
|
|||
|
|
var n int64
|
|||
|
|
s.DB.Model(&model.AppRelease{}).Where("version = ? AND channel = ?", version, channel).Count(&n)
|
|||
|
|
if n > 0 {
|
|||
|
|
return nil, commonservice.Conflict("VERSION_EXISTS")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
dir := filepath.Join(s.Cfg.StorageDir, "releases", channel)
|
|||
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|||
|
|
return nil, commonservice.Internal("SAVE_FAILED")
|
|||
|
|
}
|
|||
|
|
relName := filepath.ToSlash(filepath.Join("releases", channel, version+"-installer.exe"))
|
|||
|
|
abs := filepath.Join(s.Cfg.StorageDir, filepath.FromSlash(relName))
|
|||
|
|
f, err := os.Create(abs)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, commonservice.Internal("SAVE_FAILED")
|
|||
|
|
}
|
|||
|
|
defer f.Close()
|
|||
|
|
|
|||
|
|
h := sha256.New()
|
|||
|
|
written, err := io.Copy(io.MultiWriter(f, h), io.LimitReader(r, s.Cfg.MaxReleaseBytes()+1))
|
|||
|
|
if err != nil {
|
|||
|
|
_ = os.Remove(abs)
|
|||
|
|
return nil, commonservice.Internal("SAVE_FAILED")
|
|||
|
|
}
|
|||
|
|
if written > s.Cfg.MaxReleaseBytes() {
|
|||
|
|
_ = os.Remove(abs)
|
|||
|
|
return nil, commonservice.BadRequest("FILE_TOO_LARGE")
|
|||
|
|
}
|
|||
|
|
sum := hex.EncodeToString(h.Sum(nil))
|
|||
|
|
row := model.AppRelease{
|
|||
|
|
Version: version,
|
|||
|
|
Channel: channel,
|
|||
|
|
Filename: relName,
|
|||
|
|
SHA256: sum,
|
|||
|
|
SizeBytes: written,
|
|||
|
|
Changelog: changelog,
|
|||
|
|
CreatedAt: commonservice.NowRFC(),
|
|||
|
|
IsLatest: 0,
|
|||
|
|
}
|
|||
|
|
if err := s.DB.Create(&row).Error; err != nil {
|
|||
|
|
_ = os.Remove(abs)
|
|||
|
|
return nil, commonservice.Internal("SAVE_FAILED")
|
|||
|
|
}
|
|||
|
|
return &row, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Publish 将指定发版标为该渠道最新。
|
|||
|
|
func (s *ReleaseService) Publish(id int64) (*model.AppRelease, error) {
|
|||
|
|
var row model.AppRelease
|
|||
|
|
if err := s.DB.First(&row, id).Error; err != nil {
|
|||
|
|
return nil, commonservice.NotFound("NOT_FOUND")
|
|||
|
|
}
|
|||
|
|
tx := s.DB.Begin()
|
|||
|
|
if err := tx.Model(&model.AppRelease{}).Where("channel = ?", row.Channel).Update("is_latest", 0).Error; err != nil {
|
|||
|
|
tx.Rollback()
|
|||
|
|
return nil, commonservice.Internal("SAVE_FAILED")
|
|||
|
|
}
|
|||
|
|
if err := tx.Model(&row).Update("is_latest", 1).Error; err != nil {
|
|||
|
|
tx.Rollback()
|
|||
|
|
return nil, commonservice.Internal("SAVE_FAILED")
|
|||
|
|
}
|
|||
|
|
if err := tx.Commit().Error; err != nil {
|
|||
|
|
return nil, commonservice.Internal("SAVE_FAILED")
|
|||
|
|
}
|
|||
|
|
row.IsLatest = 1
|
|||
|
|
return &row, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// OpenFile 打开发版文件供下载。
|
|||
|
|
func (s *ReleaseService) OpenFile(version, channel string) (*model.AppRelease, *os.File, error) {
|
|||
|
|
version = strings.TrimSpace(version)
|
|||
|
|
channel = normalizeChannel(channel)
|
|||
|
|
var row model.AppRelease
|
|||
|
|
if err := s.DB.Where("version = ? AND channel = ?", version, channel).First(&row).Error; err != nil {
|
|||
|
|
return nil, nil, commonservice.NotFound("NOT_FOUND")
|
|||
|
|
}
|
|||
|
|
abs := filepath.Join(s.Cfg.StorageDir, filepath.FromSlash(row.Filename))
|
|||
|
|
f, err := os.Open(abs)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, nil, commonservice.NotFound("NOT_FOUND")
|
|||
|
|
}
|
|||
|
|
return &row, f, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeChannel(ch string) string {
|
|||
|
|
ch = strings.TrimSpace(strings.ToLower(ch))
|
|||
|
|
if ch == "" {
|
|||
|
|
return "stable"
|
|||
|
|
}
|
|||
|
|
return ch
|
|||
|
|
}
|