init: 初始化项目

This commit is contained in:
0264408
2026-03-10 16:26:48 +08:00
commit 57e0ef2cf6
79 changed files with 8943 additions and 0 deletions

255
internal/asset/service.go Normal file
View 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
}