初始化2
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/dto"
|
||||
@@ -34,6 +35,7 @@ func NewChunkUploadService() *ChunkUploadService {
|
||||
}
|
||||
|
||||
// InitUpload 初始化分片上传
|
||||
// 根据存储策略自动选择: 本地模式(服务端中转) 或 MinIO模式(客户端直传)
|
||||
func (s *ChunkUploadService) InitUpload(fileName string, fileSize int64, md5 string, folderID, ownerID, policyID uint64) (*dto.ChunkUploadInitVO, error) {
|
||||
// 秒传检查
|
||||
if md5 != "" {
|
||||
@@ -49,19 +51,28 @@ func (s *ChunkUploadService) InitUpload(fileName string, fileSize int64, md5 str
|
||||
}
|
||||
}
|
||||
|
||||
// 计算分片参数(5MB每片)
|
||||
// 计算分片参数
|
||||
chunkSize := int64(5 * 1024 * 1024)
|
||||
if fileSize < chunkSize {
|
||||
chunkSize = fileSize
|
||||
}
|
||||
totalChunks := int((fileSize + chunkSize - 1) / chunkSize)
|
||||
|
||||
// 创建临时目录
|
||||
uploadID := uuid.New().String()
|
||||
|
||||
// 生成存储路径
|
||||
ext := utils.GetExtension(fileName)
|
||||
var storageKey string
|
||||
if md5 != "" {
|
||||
storageKey = storage.GenerateStorageKey(md5, ext)
|
||||
} else {
|
||||
storageKey = storage.GenerateStorageKey(fmt.Sprintf("%d-%d", ownerID, time.Now().UnixNano()), ext)
|
||||
}
|
||||
|
||||
// 创建上传会话
|
||||
tempDir := filepath.Join(config.AppConfig.Storage.LocalBasePath, ".tmp", uploadID)
|
||||
os.MkdirAll(tempDir, 0755)
|
||||
|
||||
// 创建上传会话
|
||||
session := &model.UploadSession{
|
||||
ID: uploadID,
|
||||
FileName: fileName,
|
||||
@@ -73,22 +84,51 @@ func (s *ChunkUploadService) InitUpload(fileName string, fileSize int64, md5 str
|
||||
OwnerID: ownerID,
|
||||
PolicyID: policyID,
|
||||
Status: 1,
|
||||
TempDir: tempDir,
|
||||
}
|
||||
|
||||
if err := config.DB.Create(session).Error; err != nil {
|
||||
return nil, fmt.Errorf("创建上传会话失败: %w", err)
|
||||
TempDir: storageKey, // 复用字段存储目标storageKey
|
||||
}
|
||||
config.DB.Create(session)
|
||||
|
||||
// 检查是否为MinIO存储
|
||||
engine, err := storage.GetEngineByPolicyID(policyID)
|
||||
if err == nil {
|
||||
if minioEngine, ok := engine.(*storage.MinIOEngine); ok {
|
||||
return s.initMinIOUpload(minioEngine, session, storageKey)
|
||||
}
|
||||
}
|
||||
|
||||
// 本地存储模式
|
||||
return &dto.ChunkUploadInitVO{
|
||||
UploadID: uploadID,
|
||||
Instant: false,
|
||||
ChunkSize: chunkSize,
|
||||
Total: totalChunks,
|
||||
Mode: "local",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadChunk 上传单个分片
|
||||
// initMinIOUpload 初始化MinIO直传模式
|
||||
func (s *ChunkUploadService) initMinIOUpload(minioEngine *storage.MinIOEngine, session *model.UploadSession, storageKey string) (*dto.ChunkUploadInitVO, error) {
|
||||
// 为每个分片生成预签名上传URL
|
||||
parts := make([]dto.ChunkPresignURL, session.TotalChunks)
|
||||
for i := 0; i < session.TotalChunks; i++ {
|
||||
chunkKey := fmt.Sprintf("%s.chunk_%06d", storageKey, i)
|
||||
presignedURL, err := minioEngine.GetPresignedChunkURL(chunkKey, 24*time.Hour)
|
||||
if err != nil {
|
||||
presignedURL = ""
|
||||
}
|
||||
parts[i] = dto.ChunkPresignURL{Index: i, URL: presignedURL}
|
||||
}
|
||||
|
||||
return &dto.ChunkUploadInitVO{
|
||||
UploadID: session.ID,
|
||||
ChunkSize: session.ChunkSize,
|
||||
Total: session.TotalChunks,
|
||||
Mode: "minio",
|
||||
Parts: parts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UploadChunk 上传单个分片(本地模式)
|
||||
func (s *ChunkUploadService) UploadChunk(uploadID string, index int, reader io.Reader, checksum string) (*dto.ChunkUploadChunkVO, error) {
|
||||
var session model.UploadSession
|
||||
if err := config.DB.First(&session, uploadID).Error; err != nil {
|
||||
@@ -101,14 +141,12 @@ func (s *ChunkUploadService) UploadChunk(uploadID string, index int, reader io.R
|
||||
return nil, errors.New("分片索引无效")
|
||||
}
|
||||
|
||||
// 检查是否已上传
|
||||
uploaded := parseChunkIndices(session.UploadedChunks)
|
||||
if uploaded[index] {
|
||||
return &dto.ChunkUploadChunkVO{Index: index, Status: "exists"}, nil
|
||||
}
|
||||
|
||||
// 写入临时文件
|
||||
chunkPath := filepath.Join(session.TempDir, fmt.Sprintf("chunk_%06d", index))
|
||||
chunkPath := filepath.Join(config.AppConfig.Storage.LocalBasePath, ".tmp", uploadID, fmt.Sprintf("chunk_%06d", index))
|
||||
dst, err := os.Create(chunkPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建分片文件失败: %w", err)
|
||||
@@ -123,14 +161,12 @@ func (s *ChunkUploadService) UploadChunk(uploadID string, index int, reader io.R
|
||||
return nil, fmt.Errorf("写入分片失败: %w", err)
|
||||
}
|
||||
|
||||
// checksum校验
|
||||
actualChecksum := hex.EncodeToString(hash.Sum(nil))
|
||||
if checksum != "" && checksum != actualChecksum {
|
||||
os.Remove(chunkPath)
|
||||
return nil, fmt.Errorf("分片校验失败: 期望 %s, 实际 %s", checksum, actualChecksum)
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
uploaded[index] = true
|
||||
chunkStr := buildChunkIndices(uploaded)
|
||||
config.DB.Model(&session).Update("uploaded_chunks", chunkStr)
|
||||
@@ -138,6 +174,24 @@ func (s *ChunkUploadService) UploadChunk(uploadID string, index int, reader io.R
|
||||
return &dto.ChunkUploadChunkVO{Index: index, Size: size, Status: "ok"}, nil
|
||||
}
|
||||
|
||||
// UploadChunkMinIO 确认MinIO分片上传完成(客户端直传后调用)
|
||||
func (s *ChunkUploadService) UploadChunkMinIO(uploadID string, index int, etag string) (*dto.ChunkUploadChunkVO, error) {
|
||||
var session model.UploadSession
|
||||
if err := config.DB.First(&session, uploadID).Error; err != nil {
|
||||
return nil, errors.New("上传会话不存在")
|
||||
}
|
||||
if session.Status != 1 {
|
||||
return nil, errors.New("上传会话已结束")
|
||||
}
|
||||
|
||||
uploaded := parseChunkIndices(session.UploadedChunks)
|
||||
uploaded[index] = true
|
||||
chunkStr := buildChunkIndices(uploaded)
|
||||
config.DB.Model(&session).Update("uploaded_chunks", chunkStr)
|
||||
|
||||
return &dto.ChunkUploadChunkVO{Index: index, Status: "ok", ETag: etag}, nil
|
||||
}
|
||||
|
||||
// MergeChunks 合并分片完成上传
|
||||
func (s *ChunkUploadService) MergeChunks(uploadID string) (*dto.FileVO, error) {
|
||||
var session model.UploadSession
|
||||
@@ -148,7 +202,6 @@ func (s *ChunkUploadService) MergeChunks(uploadID string) (*dto.FileVO, error) {
|
||||
return nil, errors.New("上传会话已结束")
|
||||
}
|
||||
|
||||
// 检查完整性
|
||||
uploaded := parseChunkIndices(session.UploadedChunks)
|
||||
for i := 0; i < session.TotalChunks; i++ {
|
||||
if !uploaded[i] {
|
||||
@@ -156,14 +209,30 @@ func (s *ChunkUploadService) MergeChunks(uploadID string) (*dto.FileVO, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
mergedPath := filepath.Join(session.TempDir, "merged")
|
||||
storageKey := session.TempDir // 目标storageKey
|
||||
|
||||
// 检测MinIO模式
|
||||
engine, _ := storage.GetEngineByPolicyID(session.PolicyID)
|
||||
if minioEngine, ok := engine.(*storage.MinIOEngine); ok {
|
||||
return s.mergeMinIO(minioEngine, &session, storageKey)
|
||||
}
|
||||
|
||||
// 本地模式: 合并分片文件
|
||||
return s.mergeLocal(&session, storageKey)
|
||||
}
|
||||
|
||||
// mergeLocal 本地模式合并
|
||||
func (s *ChunkUploadService) mergeLocal(session *model.UploadSession, storageKey string) (*dto.FileVO, error) {
|
||||
tmpDir := filepath.Join(config.AppConfig.Storage.LocalBasePath, ".tmp", session.ID)
|
||||
mergedPath := filepath.Join(tmpDir, "merged")
|
||||
|
||||
merged, err := os.Create(mergedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建合并文件失败: %w", err)
|
||||
}
|
||||
|
||||
for i := 0; i < session.TotalChunks; i++ {
|
||||
chunkPath := filepath.Join(session.TempDir, fmt.Sprintf("chunk_%06d", i))
|
||||
chunkPath := filepath.Join(tmpDir, fmt.Sprintf("chunk_%06d", i))
|
||||
chunk, err := os.Open(chunkPath)
|
||||
if err != nil {
|
||||
merged.Close()
|
||||
@@ -174,22 +243,20 @@ func (s *ChunkUploadService) MergeChunks(uploadID string) (*dto.FileVO, error) {
|
||||
}
|
||||
merged.Close()
|
||||
|
||||
// 计算MD5
|
||||
fileMD5, _ := utils.FileMD5(mergedPath)
|
||||
fileSHA256, _ := utils.FileSHA256(mergedPath)
|
||||
|
||||
// 验证MD5
|
||||
if session.MD5 != "" && session.MD5 != fileMD5 {
|
||||
os.RemoveAll(session.TempDir)
|
||||
return nil, fmt.Errorf("文件MD5校验失败: 期望 %s, 实际 %s", session.MD5, fileMD5)
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("MD5校验失败: 期望 %s, 实际 %s", session.MD5, fileMD5)
|
||||
}
|
||||
|
||||
// 秒传检查
|
||||
existing, _ := s.fileRepo.FindByMD5(fileMD5)
|
||||
if existing != nil {
|
||||
os.RemoveAll(session.TempDir)
|
||||
os.RemoveAll(tmpDir)
|
||||
newFile := &model.File{
|
||||
Name: session.FileName, Extension: utils.GetExtension(session.FileName),
|
||||
ID: utils.GenID(), Name: session.FileName, Extension: utils.GetExtension(session.FileName),
|
||||
FolderID: session.FolderID, OwnerID: session.OwnerID, Size: session.FileSize,
|
||||
MD5: fileMD5, StorageKey: existing.StorageKey, StoragePolicyID: existing.StoragePolicyID,
|
||||
MimeType: utils.GetMimeType(utils.GetExtension(session.FileName)), Status: 1,
|
||||
@@ -200,36 +267,60 @@ func (s *ChunkUploadService) MergeChunks(uploadID string) (*dto.FileVO, error) {
|
||||
|
||||
// 上传到存储引擎
|
||||
ext := utils.GetExtension(session.FileName)
|
||||
storageKey := storage.GenerateStorageKey(fileMD5, ext)
|
||||
engine, err := storage.GetEngineByPolicyID(session.PolicyID)
|
||||
if err != nil {
|
||||
engine = storage.GetDefaultEngine()
|
||||
}
|
||||
engine, _ := storage.GetEngineByPolicyID(session.PolicyID)
|
||||
reader, _ := os.Open(mergedPath)
|
||||
defer reader.Close()
|
||||
if err := engine.Upload(storageKey, reader, session.FileSize); err != nil {
|
||||
os.RemoveAll(session.TempDir)
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("存储文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存记录
|
||||
newFile := &model.File{
|
||||
Name: session.FileName, Extension: ext, FolderID: session.FolderID, OwnerID: session.OwnerID,
|
||||
ID: utils.GenID(), Name: session.FileName, Extension: ext, FolderID: session.FolderID, OwnerID: session.OwnerID,
|
||||
Size: session.FileSize, MD5: fileMD5, SHA256: fileSHA256, StorageKey: storageKey,
|
||||
StoragePolicyID: session.PolicyID, MimeType: utils.GetMimeType(ext), Status: 1,
|
||||
}
|
||||
if err := s.fileRepo.Create(newFile); err != nil {
|
||||
engine.Delete(storageKey)
|
||||
os.RemoveAll(session.TempDir)
|
||||
os.RemoveAll(tmpDir)
|
||||
return nil, fmt.Errorf("保存文件记录失败: %w", err)
|
||||
}
|
||||
|
||||
config.DB.Model(&session).Updates(map[string]interface{}{"status": 2, "file_id": newFile.ID, "md5": fileMD5, "sha256": fileSHA256})
|
||||
os.RemoveAll(session.TempDir)
|
||||
config.DB.Model(session).Updates(map[string]interface{}{"status": 2, "file_id": newFile.ID, "md5": fileMD5, "sha256": fileSHA256})
|
||||
os.RemoveAll(tmpDir)
|
||||
return fileToVO(newFile), nil
|
||||
}
|
||||
|
||||
// GetUploadProgress 查询上传进度(断点续传)
|
||||
// mergeMinIO MinIO模式合并
|
||||
func (s *ChunkUploadService) mergeMinIO(minioEngine *storage.MinIOEngine, session *model.UploadSession, storageKey string) (*dto.FileVO, error) {
|
||||
// 构建分片key列表
|
||||
chunkKeys := make([]string, session.TotalChunks)
|
||||
for i := 0; i < session.TotalChunks; i++ {
|
||||
chunkKeys[i] = fmt.Sprintf("%s.chunk_%06d", storageKey, i)
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
if err := minioEngine.MergeChunks(chunkKeys, storageKey); err != nil {
|
||||
return nil, fmt.Errorf("MinIO合并失败: %w", err)
|
||||
}
|
||||
|
||||
ext := utils.GetExtension(session.FileName)
|
||||
fileMD5 := session.MD5 // 使用客户端提供的MD5
|
||||
|
||||
newFile := &model.File{
|
||||
ID: utils.GenID(), Name: session.FileName, Extension: ext, FolderID: session.FolderID, OwnerID: session.OwnerID,
|
||||
Size: session.FileSize, MD5: fileMD5, StorageKey: storageKey,
|
||||
StoragePolicyID: session.PolicyID, MimeType: utils.GetMimeType(ext), Status: 1,
|
||||
}
|
||||
if err := s.fileRepo.Create(newFile); err != nil {
|
||||
return nil, fmt.Errorf("保存文件记录失败: %w", err)
|
||||
}
|
||||
|
||||
config.DB.Model(session).Updates(map[string]interface{}{"status": 2, "file_id": newFile.ID, "md5": fileMD5})
|
||||
return fileToVO(newFile), nil
|
||||
}
|
||||
|
||||
// GetUploadProgress 查询上传进度
|
||||
func (s *ChunkUploadService) GetUploadProgress(uploadID string) (*dto.ChunkUploadProgressVO, error) {
|
||||
var session model.UploadSession
|
||||
if err := config.DB.First(&session, uploadID).Error; err != nil {
|
||||
@@ -255,7 +346,7 @@ func (s *ChunkUploadService) CancelUpload(uploadID string) error {
|
||||
if err := config.DB.First(&session, uploadID).Error; err != nil {
|
||||
return errors.New("上传会话不存在")
|
||||
}
|
||||
os.RemoveAll(session.TempDir)
|
||||
os.RemoveAll(filepath.Join(config.AppConfig.Storage.LocalBasePath, ".tmp", uploadID))
|
||||
config.DB.Model(&session).Update("status", 3)
|
||||
return nil
|
||||
}
|
||||
@@ -288,7 +379,8 @@ func buildChunkIndices(m map[int]bool) string {
|
||||
|
||||
func fileToVO(file *model.File) *dto.FileVO {
|
||||
vo := &dto.FileVO{
|
||||
ID: file.ID, Name: file.Name, Extension: file.Extension, FolderID: file.FolderID,
|
||||
ID: dto.StringID(file.ID), Name: file.Name, Extension: file.Extension, FolderID: dto.StringID(file.FolderID),
|
||||
OwnerID: dto.StringID(file.OwnerID),
|
||||
Size: file.Size, StorageKey: file.StorageKey, StoragePolicyID: file.StoragePolicyID,
|
||||
MimeType: file.MimeType, IsFavorite: file.IsFavorite, DownloadCount: file.DownloadCount,
|
||||
Version: file.Version, CreatedAt: file.CreatedAt, UpdatedAt: file.UpdatedAt,
|
||||
@@ -299,6 +391,8 @@ func fileToVO(file *model.File) *dto.FileVO {
|
||||
if localEngine, ok := engine.(*storage.LocalEngine); ok {
|
||||
vo.DownloadURL = localEngine.GetURL(file.StorageKey)
|
||||
vo.PreviewURL = localEngine.GetURL(file.StorageKey)
|
||||
} else if minioEngine, ok := engine.(*storage.MinIOEngine); ok {
|
||||
vo.PreviewURL = minioEngine.GetPreviewURL(file.StorageKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user