256 lines
6.5 KiB
Go
256 lines
6.5 KiB
Go
package asset
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"nebula/internal/storage"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type AssetService struct {
|
|
db *gorm.DB
|
|
storage storage.Storage
|
|
}
|
|
|
|
func NewService(db *gorm.DB, storage storage.Storage) *AssetService {
|
|
return &AssetService{
|
|
db: db,
|
|
storage: storage,
|
|
}
|
|
}
|
|
|
|
// List 获取所有资源列表
|
|
func (s *AssetService) List() ([]Asset, error) {
|
|
var assets []Asset
|
|
err := s.db.Order("created_at DESC").Find(&assets).Error
|
|
return assets, err
|
|
}
|
|
|
|
// ListByRelease 获取指定发布版本的所有资源
|
|
func (s *AssetService) ListByRelease(releaseID uint) ([]Asset, error) {
|
|
var assets []Asset
|
|
err := s.db.Where("release_id = ?", releaseID).Find(&assets).Error
|
|
return assets, err
|
|
}
|
|
|
|
// Get 获取单个资源详情
|
|
func (s *AssetService) Get(id uint) (*Asset, error) {
|
|
var asset Asset
|
|
err := s.db.First(&asset, id).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("asset not found")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &asset, nil
|
|
}
|
|
|
|
// GetByReleaseAndPlatform 根据发布版本、平台和架构获取资源
|
|
func (s *AssetService) GetByReleaseAndPlatform(releaseID uint, platform, arch string) (*Asset, error) {
|
|
var asset Asset
|
|
err := s.db.Where("release_id = ? AND platform = ? AND arch = ?", releaseID, platform, arch).
|
|
First(&asset).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, errors.New("asset not found for this platform and architecture")
|
|
}
|
|
return nil, err
|
|
}
|
|
return &asset, nil
|
|
}
|
|
|
|
// Upload 上传文件并创建资源记录
|
|
func (s *AssetService) Upload(releaseID uint, platform, arch string, file *multipart.FileHeader) (*Asset, error) {
|
|
// 检查 Release 是否存在
|
|
var count int64
|
|
err := s.db.Table("releases").Where("id = ?", releaseID).Count(&count).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if count == 0 {
|
|
return nil, errors.New("release not found")
|
|
}
|
|
|
|
// 检查是否已存在相同平台和架构的资源
|
|
var existing Asset
|
|
err = s.db.Where("release_id = ? AND platform = ? AND arch = ?", releaseID, platform, arch).
|
|
First(&existing).Error
|
|
if err == nil {
|
|
return nil, errors.New("asset already exists for this platform and architecture")
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
|
|
// 打开上传的文件
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open uploaded file: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
// 计算文件校验和
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, src); err != nil {
|
|
return nil, fmt.Errorf("failed to calculate checksum: %w", err)
|
|
}
|
|
checksum := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
// 重新打开文件用于保存(因为已经读取过了)
|
|
src.Close()
|
|
src, err = file.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to reopen file: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
// 构建存储路径: releases/{releaseID}/{platform}-{arch}/{filename}
|
|
storagePath := fmt.Sprintf("releases/%d/%s-%s/%s", releaseID, platform, arch, file.Filename)
|
|
|
|
// 保存文件
|
|
savedPath, err := s.storage.Save(storagePath, src)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to save file: %w", err)
|
|
}
|
|
|
|
// 获取文件访问 URL
|
|
url := s.storage.GetURL(savedPath)
|
|
|
|
// 创建数据库记录
|
|
asset := Asset{
|
|
ReleaseID: releaseID,
|
|
Platform: platform,
|
|
Arch: arch,
|
|
URL: url,
|
|
StoragePath: savedPath,
|
|
Checksum: checksum,
|
|
Signature: "", // TODO: 实现文件签名
|
|
}
|
|
|
|
if err := s.db.Create(&asset).Error; err != nil {
|
|
// 如果数据库操作失败,删除已上传的文件
|
|
s.storage.Delete(savedPath)
|
|
return nil, err
|
|
}
|
|
|
|
return &asset, nil
|
|
}
|
|
|
|
// Create 创建资源记录(用于外部URL)
|
|
func (s *AssetService) Create(asset Asset) error {
|
|
// 检查 Release 是否存在
|
|
var count int64
|
|
err := s.db.Table("releases").Where("id = ?", asset.ReleaseID).Count(&count).Error
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return errors.New("release not found")
|
|
}
|
|
|
|
// 检查是否已存在相同平台和架构的资源
|
|
err = s.db.Where("release_id = ? AND platform = ? AND arch = ?", asset.ReleaseID, asset.Platform, asset.Arch).
|
|
First(&Asset{}).Error
|
|
if err == nil {
|
|
return errors.New("asset already exists for this platform and architecture")
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
|
|
return s.db.Create(&asset).Error
|
|
}
|
|
|
|
// Update 更新资源信息(不包括文件)
|
|
func (s *AssetService) Update(id uint, data map[string]any) error {
|
|
// 检查资源是否存在
|
|
var asset Asset
|
|
err := s.db.First(&asset, id).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errors.New("asset not found")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// 如果更新平台或架构,检查是否会造成重复
|
|
newPlatform, hasPlatform := data["platform"].(string)
|
|
newArch, hasArch := data["arch"].(string)
|
|
|
|
if hasPlatform || hasArch {
|
|
checkPlatform := asset.Platform
|
|
checkArch := asset.Arch
|
|
if hasPlatform {
|
|
checkPlatform = newPlatform
|
|
}
|
|
if hasArch {
|
|
checkArch = newArch
|
|
}
|
|
|
|
err := s.db.Where("release_id = ? AND platform = ? AND arch = ? AND id != ?",
|
|
asset.ReleaseID, checkPlatform, checkArch, id).
|
|
First(&Asset{}).Error
|
|
if err == nil {
|
|
return errors.New("asset already exists for this platform and architecture")
|
|
}
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return s.db.Model(&Asset{}).Where("id = ?", id).Updates(data).Error
|
|
}
|
|
|
|
// Delete 删除资源(包括文件)
|
|
func (s *AssetService) Delete(id uint) error {
|
|
// 获取资源信息
|
|
var asset Asset
|
|
err := s.db.First(&asset, id).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errors.New("asset not found")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// 删除存储的文件
|
|
if asset.StoragePath != "" {
|
|
if err := s.storage.Delete(asset.StoragePath); err != nil {
|
|
// 记录错误但继续删除数据库记录
|
|
// TODO: 添加日志
|
|
}
|
|
}
|
|
|
|
// 删除数据库记录
|
|
return s.db.Delete(&Asset{}, id).Error
|
|
}
|
|
|
|
// GetStoragePath 获取资源的存储路径
|
|
func (s *AssetService) GetStoragePath(id uint) (string, error) {
|
|
var asset Asset
|
|
err := s.db.First(&asset, id).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", errors.New("asset not found")
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
if asset.StoragePath == "" {
|
|
return "", errors.New("storage path not available")
|
|
}
|
|
|
|
// 如果是本地存储,返回完整路径
|
|
if localStorage, ok := s.storage.(*storage.LocalStorage); ok {
|
|
return localStorage.GetFullPath(asset.StoragePath), nil
|
|
}
|
|
|
|
return asset.StoragePath, nil
|
|
}
|