464 lines
6.4 KiB
Go
464 lines
6.4 KiB
Go
|
package service
|
|||
|
|
|||
|
import (
|
|||
|
"errors"
|
|||
|
|
|||
|
"fmt"
|
|||
|
|
|||
|
"io"
|
|||
|
|
|||
|
"mime/multipart"
|
|||
|
|
|||
|
"os"
|
|||
|
|
|||
|
"path/filepath"
|
|||
|
|
|||
|
"sort"
|
|||
|
|
|||
|
"strings"
|
|||
|
|
|||
|
"time"
|
|||
|
|
|||
|
"licserver/internal/model"
|
|||
|
|
|||
|
"licserver/internal/utils"
|
|||
|
|
|||
|
"github.com/google/uuid"
|
|||
|
|
|||
|
"gorm.io/gorm"
|
|||
|
|
|||
|
"crypto/sha256"
|
|||
|
|
|||
|
"encoding/hex"
|
|||
|
)
|
|||
|
|
|||
|
type UploadService struct {
|
|||
|
db *gorm.DB
|
|||
|
|
|||
|
config *utils.Config
|
|||
|
}
|
|||
|
|
|||
|
func NewUploadService(db *gorm.DB, config *utils.Config) *UploadService {
|
|||
|
|
|||
|
return &UploadService{
|
|||
|
|
|||
|
db: db,
|
|||
|
|
|||
|
config: config,
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) UploadFile(file *multipart.FileHeader, userID uint, deviceUID, description string) (*model.FileUpload, error) {
|
|||
|
|
|||
|
// 生成唯一文件名
|
|||
|
|
|||
|
ext := filepath.Ext(file.Filename)
|
|||
|
|
|||
|
uniqueID := uuid.New().String()
|
|||
|
|
|||
|
fileName := fmt.Sprintf("%s%s", uniqueID, ext)
|
|||
|
|
|||
|
filePath := filepath.Join(s.config.Upload.Path, fileName)
|
|||
|
|
|||
|
// 确保上传目录存在
|
|||
|
|
|||
|
if err := os.MkdirAll(s.config.Upload.Path, 0755); err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 保存文件
|
|||
|
|
|||
|
src, err := file.Open()
|
|||
|
|
|||
|
if err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
defer src.Close()
|
|||
|
|
|||
|
dst, err := os.Create(filePath)
|
|||
|
|
|||
|
if err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
defer dst.Close()
|
|||
|
|
|||
|
if _, err = io.Copy(dst, src); err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 如果提供了设备UID,获取设备型号
|
|||
|
|
|||
|
var deviceModel string
|
|||
|
|
|||
|
if deviceUID != "" {
|
|||
|
|
|||
|
var device model.Device
|
|||
|
|
|||
|
if err := s.db.Where("uid = ?", deviceUID).First(&device).Error; err == nil {
|
|||
|
|
|||
|
deviceModel = device.DeviceModel
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 创建数据库记录
|
|||
|
|
|||
|
upload := &model.FileUpload{
|
|||
|
|
|||
|
FileName: file.Filename,
|
|||
|
|
|||
|
FilePath: filePath,
|
|||
|
|
|||
|
FileSize: file.Size,
|
|||
|
|
|||
|
FileType: strings.ToLower(ext),
|
|||
|
|
|||
|
UploadedBy: userID,
|
|||
|
|
|||
|
DeviceUID: deviceUID,
|
|||
|
|
|||
|
DeviceModel: deviceModel,
|
|||
|
|
|||
|
Description: description,
|
|||
|
}
|
|||
|
|
|||
|
if err := s.db.Create(upload).Error; err != nil {
|
|||
|
|
|||
|
os.Remove(filePath)
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return upload, nil
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) DownloadFile(id uint) (*model.FileUpload, error) {
|
|||
|
|
|||
|
var file model.FileUpload
|
|||
|
|
|||
|
if err := s.db.First(&file, id).Error; err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return &file, nil
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) DeleteFile(id uint, userID uint) error {
|
|||
|
|
|||
|
var file model.FileUpload
|
|||
|
|
|||
|
if err := s.db.First(&file, id).Error; err != nil {
|
|||
|
|
|||
|
return err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
if file.UploadedBy != userID {
|
|||
|
|
|||
|
return errors.New("无权删除此文件")
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
if err := os.Remove(file.FilePath); err != nil && !os.IsNotExist(err) {
|
|||
|
|
|||
|
return err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return s.db.Delete(&file).Error
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) GetDeviceFiles(deviceUID string) ([]model.FileUpload, error) {
|
|||
|
|
|||
|
var files []model.FileUpload
|
|||
|
|
|||
|
err := s.db.Where("device_uid = ?", deviceUID).Find(&files).Error
|
|||
|
|
|||
|
return files, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) UploadChunk(
|
|||
|
|
|||
|
file *multipart.FileHeader,
|
|||
|
|
|||
|
fileHash string,
|
|||
|
|
|||
|
chunkNumber int,
|
|||
|
|
|||
|
totalChunks int,
|
|||
|
|
|||
|
totalSize int64,
|
|||
|
|
|||
|
filename string,
|
|||
|
|
|||
|
userID uint,
|
|||
|
|
|||
|
deviceUID string,
|
|||
|
|
|||
|
) error {
|
|||
|
|
|||
|
// 创建分片存储目录
|
|||
|
|
|||
|
chunkDir := filepath.Join(s.config.Upload.Path, "chunks", fileHash)
|
|||
|
|
|||
|
if err := os.MkdirAll(chunkDir, 0755); err != nil {
|
|||
|
|
|||
|
return err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 保存分片文件
|
|||
|
|
|||
|
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("%d", chunkNumber))
|
|||
|
|
|||
|
src, err := file.Open()
|
|||
|
|
|||
|
if err != nil {
|
|||
|
|
|||
|
return err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
defer src.Close()
|
|||
|
|
|||
|
dst, err := os.Create(chunkPath)
|
|||
|
|
|||
|
if err != nil {
|
|||
|
|
|||
|
return err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
defer dst.Close()
|
|||
|
|
|||
|
if _, err = io.Copy(dst, src); err != nil {
|
|||
|
|
|||
|
return err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 记录分片信息
|
|||
|
|
|||
|
chunk := model.UploadChunk{
|
|||
|
|
|||
|
FileHash: fileHash,
|
|||
|
|
|||
|
ChunkNumber: chunkNumber,
|
|||
|
|
|||
|
ChunkSize: file.Size,
|
|||
|
|
|||
|
ChunkPath: chunkPath,
|
|||
|
|
|||
|
TotalChunks: totalChunks,
|
|||
|
|
|||
|
TotalSize: totalSize,
|
|||
|
|
|||
|
Filename: filename,
|
|||
|
|
|||
|
FileType: strings.ToLower(filepath.Ext(filename)),
|
|||
|
|
|||
|
UploadedBy: userID,
|
|||
|
|
|||
|
DeviceUID: deviceUID,
|
|||
|
}
|
|||
|
|
|||
|
return s.db.Create(&chunk).Error
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) CheckUploadStatus(fileHash string) (bool, error) {
|
|||
|
|
|||
|
var chunks []model.UploadChunk
|
|||
|
|
|||
|
if err := s.db.Where("file_hash = ?", fileHash).Find(&chunks).Error; err != nil {
|
|||
|
|
|||
|
return false, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
if len(chunks) == 0 {
|
|||
|
|
|||
|
return false, nil
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
totalChunks := chunks[0].TotalChunks
|
|||
|
|
|||
|
return len(chunks) == totalChunks, nil
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) MergeChunks(fileHash string) (*model.FileUpload, error) {
|
|||
|
|
|||
|
var chunks []model.UploadChunk
|
|||
|
|
|||
|
if err := s.db.Where("file_hash = ?", fileHash).Find(&chunks).Error; err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
if len(chunks) == 0 {
|
|||
|
|
|||
|
return nil, errors.New("未找到文件分片")
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
if len(chunks) != chunks[0].TotalChunks {
|
|||
|
|
|||
|
return nil, errors.New("文件分片不完整")
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 按分片序号排序
|
|||
|
|
|||
|
sort.Slice(chunks, func(i, j int) bool {
|
|||
|
|
|||
|
return chunks[i].ChunkNumber < chunks[j].ChunkNumber
|
|||
|
|
|||
|
})
|
|||
|
|
|||
|
// 创建最终文件
|
|||
|
|
|||
|
finalPath := filepath.Join(s.config.Upload.Path, fmt.Sprintf("%s%s", uuid.New().String(), chunks[0].FileType))
|
|||
|
|
|||
|
finalFile, err := os.Create(finalPath)
|
|||
|
|
|||
|
if err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
defer finalFile.Close()
|
|||
|
|
|||
|
// 合并分片
|
|||
|
|
|||
|
hash := sha256.New()
|
|||
|
|
|||
|
for _, chunk := range chunks {
|
|||
|
|
|||
|
chunkFile, err := os.Open(chunk.ChunkPath)
|
|||
|
|
|||
|
if err != nil {
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
if _, err = io.Copy(finalFile, chunkFile); err != nil {
|
|||
|
|
|||
|
chunkFile.Close()
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
if _, err = io.Copy(hash, chunkFile); err != nil {
|
|||
|
|
|||
|
chunkFile.Close()
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
chunkFile.Close()
|
|||
|
|
|||
|
os.Remove(chunk.ChunkPath) // 删除已合并的分片
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 验证文件哈希
|
|||
|
|
|||
|
if hex.EncodeToString(hash.Sum(nil)) != fileHash {
|
|||
|
|
|||
|
os.Remove(finalPath)
|
|||
|
|
|||
|
return nil, errors.New("文件哈希验证失败")
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 创建文件记录
|
|||
|
|
|||
|
upload := &model.FileUpload{
|
|||
|
|
|||
|
FileName: chunks[0].Filename,
|
|||
|
|
|||
|
FilePath: finalPath,
|
|||
|
|
|||
|
FileSize: chunks[0].TotalSize,
|
|||
|
|
|||
|
FileType: chunks[0].FileType,
|
|||
|
|
|||
|
UploadedBy: chunks[0].UploadedBy,
|
|||
|
|
|||
|
DeviceUID: chunks[0].DeviceUID,
|
|||
|
}
|
|||
|
|
|||
|
if err := s.db.Create(upload).Error; err != nil {
|
|||
|
|
|||
|
os.Remove(finalPath)
|
|||
|
|
|||
|
return nil, err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// 清理分片记录
|
|||
|
|
|||
|
s.db.Where("file_hash = ?", fileHash).Delete(&model.UploadChunk{})
|
|||
|
|
|||
|
os.RemoveAll(filepath.Dir(chunks[0].ChunkPath))
|
|||
|
|
|||
|
return upload, nil
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func (s *UploadService) CleanupExpiredChunks() error {
|
|||
|
|
|||
|
expireTime := time.Now().Add(-24 * time.Hour)
|
|||
|
|
|||
|
var expiredChunks []model.UploadChunk
|
|||
|
|
|||
|
if err := s.db.Where("completed = ? AND created_at < ?", false, expireTime).Find(&expiredChunks).Error; err != nil {
|
|||
|
|
|||
|
return err
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
for _, chunk := range expiredChunks {
|
|||
|
|
|||
|
os.Remove(chunk.ChunkPath)
|
|||
|
|
|||
|
if len(chunk.ChunkPath) > 0 {
|
|||
|
|
|||
|
chunkDir := filepath.Dir(chunk.ChunkPath)
|
|||
|
|
|||
|
os.RemoveAll(chunkDir)
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
return s.db.Unscoped().Where("completed = ? AND created_at < ?", false, expireTime).Delete(&model.UploadChunk{}).Error
|
|||
|
|
|||
|
}
|