init: 初始化项目
This commit is contained in:
255
internal/asset/service.go
Normal file
255
internal/asset/service.go
Normal file
@@ -0,0 +1,255 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user