文章目录
Softhub软件下载站实战开发(十一):软件分片上传接口实现 🚀
前言
上一篇文章中我们实现了图片和视频的上传下载,本文聚焦于软件的上传下载,和图片视频不同,软件小则几兆,多则几GB,单次上传占据巨大内存不提,还容易失败,为了解决这些问题,我们为Softhub软件下载站实现了分片上传功能。下面将详细介绍分片上传的实现原理和代码逻辑。
分片上传原理 🧩
分片上传是将大文件分割成多个小块(chunk)分别上传,最后在服务器端合并的技术。其优势在于:
- 断点续传:某个分片上传失败可以单独重传
- 并行上传:可以同时上传多个分片提高速度
- 稳定性:小文件上传成功率更高
如果软件存储到minio,合并后还需要再次上传到minio,minio提供的api已经是分片上传,无需手动实现。
接口设计 📡
我们设计了三个核心接口:
- 初始化上传:
/dsSoftwareResource/chunk/init
- 上传分片:
/dsSoftwareResource/chunk/upload
- 合并分片:
/dsSoftwareResource/chunk/merge
代码实现 💻
1. 初始化上传接口
// 初始化分片上传
func (s *sDsSoftwareResource) InitChunkUpload(ctx context.Context, req *api.ChunkInitReq) (res *api.ChunkInitRes, err error) {
err = g.Try(ctx, func(ctx context.Context) {
// 转换softwareId
softwareId, err := convertToInt64(req.SoftwareId)
liberr.ErrIsNil(ctx, err, "软件ID格式错误")
// 检查软件是否存在
software, err := dao.DsSoftware.Ctx(ctx).Where(dao.DsSoftware.Columns().Id, softwareId).One()
liberr.ErrIsNil(ctx, err, "获取软件信息失败")
if software == nil {
liberr.ErrIsNil(ctx, fmt.Errorf("软件不存在"))
}
// 生成上传ID
uploadId := gconv.String(gtime.TimestampNano())
// 创建临时目录
tempDir := gfile.Join(gfile.Temp(), "upload", fmt.Sprintf("%d_%s", softwareId, uploadId))
if err := gfile.Mkdir(tempDir); err != nil {
liberr.ErrIsNil(ctx, err, "创建临时目录失败")
}
res = &api.ChunkInitRes{
UploadId: uploadId,
}
})
return
}
2. 上传分片接口
func (s *sDsSoftwareResource) UploadChunk(ctx context.Context, req *api.ChunkUploadReq) (res *api.ChunkUploadRes, err error) {
err = g.Try(ctx, func(ctx context.Context) {
// 转换softwareId
softwareId, err := convertToInt64(req.SoftwareId)
liberr.ErrIsNil(ctx, err, "软件ID格式错误")
// 验证上传会话
tempDir := gfile.Join(gfile.Temp(), "upload", fmt.Sprintf("%d_%s", softwareId, req.UploadId))
if !gfile.Exists(tempDir) {
liberr.ErrIsNil(ctx, fmt.Errorf("无效的上传会话"))
}
// 保存分片
chunkFile := gfile.Join(tempDir, fmt.Sprintf("chunk_%d", req.ChunkIndex))
file, err := req.File.Open()
if err != nil {
liberr.ErrIsNil(ctx, err, "打开分片文件失败")
}
defer file.Close()
// 读取文件内容
fileBytes, err := io.ReadAll(file)
if err != nil {
liberr.ErrIsNil(ctx, err, "读取分片文件失败")
}
// 保存分片
if err := gfile.PutBytes(chunkFile, fileBytes); err != nil {
liberr.ErrIsNil(ctx, err, "保存分片失败")
}
res = &api.ChunkUploadRes{
Success: true,
}
})
return
}
3. 合并分片接口
func (s *sDsSoftwareResource) MergeChunks(ctx context.Context, req *api.ChunkMergeReq) (res *api.ChunkMergeRes, err error) {
err = g.Try(ctx, func(ctx context.Context) {
// 转换softwareId
softwareId, err := convertToInt64(req.SoftwareId)
liberr.ErrIsNil(ctx, err, "软件ID格式错误")
// 验证上传会话
tempDir := gfile.Join(gfile.Temp(), "upload", fmt.Sprintf("%d_%s", softwareId, req.UploadId))
g.Log().Debug(ctx, "临时目录路径:", tempDir)
if !gfile.Exists(tempDir) {
liberr.ErrIsNil(ctx, fmt.Errorf("无效的上传会话"))
}
// 获取分片文件列表
chunkFiles, err := gfile.ScanDir(tempDir, "chunk_*")
liberr.ErrIsNil(ctx, err, "获取分片文件列表失败")
g.Log().Debug(ctx, "分片文件列表:", chunkFiles)
// 合并分片
mergedFile := gfile.Join(tempDir, "merged")
g.Log().Debug(ctx, "合并文件路径:", mergedFile)
outFile, err := gfile.OpenFile(mergedFile, os.O_CREATE|os.O_WRONLY, 0644)
liberr.ErrIsNil(ctx, err, "创建合并文件失败")
for _, chunkFile := range chunkFiles {
chunkData := gfile.GetBytes(chunkFile)
if _, err := outFile.Write(chunkData); err != nil {
liberr.ErrIsNil(ctx, err, "写入合并文件失败")
}
}
// 关闭写入文件句柄
outFile.Close()
// 计算MD5
md5, err := gmd5.EncryptFile(mergedFile)
liberr.ErrIsNil(ctx, err, "计算MD5失败")
// 上传到MinIO
drive := storage.MinioDrive{}
// 构建存储路径:software/year/month/day/md5.softhub
now := gtime.Now()
objectName := fmt.Sprintf("software/%d/%02d/%02d/%s.softhub",
now.Year(),
now.Month(),
now.Day(),
md5)
g.Log().Debug(ctx, "MinIO对象名称:", objectName)
// 获取文件信息
fileInfo, err := os.Stat(mergedFile)
liberr.ErrIsNil(ctx, err, "获取文件信息失败")
g.Log().Debug(ctx, "文件信息:", map[string]interface{}{
"name": fileInfo.Name(),
"size": fileInfo.Size(),
"mode": fileInfo.Mode(),
})
// 打开文件用于上传
file, err := os.Open(mergedFile)
liberr.ErrIsNil(ctx, err, "打开文件失败")
// 直接使用MinIO客户端上传
client, err := drive.GetClient()
liberr.ErrIsNil(ctx, err, "获取MinIO客户端失败")
opts := minio.PutObjectOptions{
ContentType: "application/octet-stream",
}
_, err = client.PutObject(ctx, config.MINIO_BUCKET, objectName, file, fileInfo.Size(), opts)
liberr.ErrIsNil(ctx, err, "上传到MinIO失败")
// 保存资源信息
// 使用雪花算法生成资源ID
resourceId := node.Generate().Int64()
_, err = dao.DsSoftwareResource.Ctx(ctx).Insert(do.DsSoftwareResource{
Id: resourceId, // 使用雪花算法生成的ID
SoftwareId: softwareId, // 软件id
ResourceName: req.ResourceName, // 软件名称
OriginName: req.ResourceName, // 原始名称
Version: req.Version, // 版本
Md5: md5, // md5
DownloadCount: 0, // 下载次数
Size: fileInfo.Size(), // 大小
ResourceUrl: objectName, // 资源路径
Default: false, // 是否默认
Remark: req.Remark, // 备注
CreatedBy: SystemS.Context().GetUserId(ctx),
UpdatedBy: SystemS.Context().GetUserId(ctx),
})
liberr.ErrIsNil(ctx, err, "保存资源信息失败")
res = &api.ChunkMergeRes{
ResourceId: resourceId,
FileUrl: fmt.Sprintf("/minio/software/%s", objectName),
}
outFile.Close()
file.Close()
if err := os.RemoveAll(tempDir); err != nil {
g.Log().Error(ctx, "强制删除临时目录失败:", tempDir, err)
}
})
return
}
至此,软件管理的核心接口已经准备完成,后续可以编写前端页面进行对接。
3. 附录
3.1 id转换
在使用雪花算法(Snowflake)生成ID时,前端传递到后端时可能会出现精度损失,导致ID值发生变化。例如:
- 原始ID:717695602914721792
- 前端接收后:717695602914721800
因此前端需要传输字符串类型,后端进行转换
api定义
// 分片上传初始化请求
type ChunkInitReq struct {
g.Meta `path:"/dsSoftwareResource/chunk/init" method:"post" tags:"软件资源表" summary:"分片上传-初始化"`
SoftwareId interface{} `json:"softwareId" v:"required#软件ID不能为空"`
FileName string `json:"fileName" v:"required#文件名不能为空"`
FileSize int64 `json:"fileSize" v:"required#文件大小不能为空"`
ChunkSize int64 `json:"chunkSize" v:"required#分片大小不能为空"`
}
SoftwareId要定义成interface{},接口中进行转换
转换函数
func convertToInt64(id interface{}) (int64, error) {
switch v := id.(type) {
case int64:
return v, nil
case int:
return int64(v), nil
case uint:
return int64(v), nil
case uint64:
return int64(v), nil
case string:
return strconv.ParseInt(v, 10, 64)
case float64:
return int64(v), nil
default:
return 0, fmt.Errorf("unsupported ID type: %T", id)
}
}
softhub系列往期文章
- Softhub软件下载站实战开发(一):项目总览
- Softhub软件下载站实战开发(二):项目基础框架搭建
- Softhub软件下载站实战开发(三):平台管理模块实战
- Softhub软件下载站实战开发(四):代码生成器设计与实现
- Softhub软件下载站实战开发(五):分类模块实现
- Softhub软件下载站实战开发(六):软件配置面板实现
- Softhub软件下载站实战开发(七):集成MinIO实现文件存储功能
- Softhub软件下载站实战开发(八):编写软件后台管理
- Softhub软件下载站实战开发(九):编写软件配置管理界面
- Softhub软件下载站实战开发(十):实现图片视频上传下载接口