https://codeberg.org/meowrain/fileuploadwithchunk
后端部分
后端部分采用gin实现
r.POST("/upload-chunk", uploadChunk)
r.POST("/get-uploaded-chunks", getUploadedChunks)
r.POST("/merge-chunks", mergeChunks)
r.GET("/download/:filename", downloadFile)
路由如上,/upload-chunk
负责处理上传业务,get-uploaded-chunks
负责获取已经上传的切片数组,merge-chunks
负责在upload-chunk结束后,前端通知后端对上传的分片进行合并,/download/:filename
用于从合并的文件中找到文件并返回给前端
首选初始化项目
go mod init fileupload
go get -u "github.com/gin-gonic/gin"
uploadChunk实现
分析
这里要接收三个form参数
- file 字段 文件二进制
- chunkIndex 在上传的分片索引
- totalChunks 总分片数量
- fileMD5 文件MD5
我们用后端从里面提取出这些参数
file, err := c.FormFile("file")
if err != nil {
log.Println("Failed to get file from form:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
chunkIndex := c.PostForm("chunkIndex")
totalChunks := c.PostForm("totalChunks")
fileMd5 := c.PostForm("fileMD5")
index, _ := strconv.Atoi(chunkIndex)
index = index + 1
fmt.Println("Received chunk:", index, "of", totalChunks, "for file:", fileMd5)
接下来要定义一个存储分片文件的文件夹 uploads,
uploadDir := "./uploads"
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
log.Println("Failed to create upload directory:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
然后我们要给分片进行命名
可以直接命名成 文件名+文件md5+切片索引的格式
然后进行路径拼接,将前面的uploads/ 和现在的文件名拼接在一起
chunkFilename := fmt.Sprintf("%s_%s_%s", file.Filename, fileMd5, chunkIndex)
filePath := filepath.Join(uploadDir, chunkFilename)
对分片进行存储
// 创建分片文件,
dst, err := os.Create(filePath)
if err != nil {
log.Println("Failed to create chunk file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer dst.Close()
// 打开被上传过来的二进制文件
src, err := file.Open()
if err != nil {
log.Println("Failed to open uploaded file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer src.Close()
// 把file中的数据读取出来,拷贝到刚刚创建的分片文件中
if _, err := io.Copy(dst, src); err != nil {
log.Println("Failed to save chunk file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
如果上面的步骤全部没有错误地完成,响应成功
c.JSON(http.StatusOK, gin.H{"success": true, "message": "分片上传成功"})
完整代码
func uploadChunk(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
log.Println("Failed to get file from form:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
chunkIndex := c.PostForm("chunkIndex")
totalChunks := c.PostForm("totalChunks")
fileMd5 := c.PostForm("fileMD5")
index, _ := strconv.Atoi(chunkIndex)
index = index + 1
fmt.Println("Received chunk:", index, "of", totalChunks, "for file:", fileMd5)
uploadDir := "./uploads"
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
log.Println("Failed to create upload directory:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
chunkFilename := fmt.Sprintf("%s_%s_%s", file.Filename, fileMd5, chunkIndex)
filePath := filepath.Join(uploadDir, chunkFilename)
// 保存分片文件
dst, err := os.Create(filePath)
if err != nil {
log.Println("Failed to create chunk file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer dst.Close()
src, err := file.Open()
if err != nil {
log.Println("Failed to open uploaded file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer src.Close()
if _, err := io.Copy(dst, src); err != nil {
log.Println("Failed to save chunk file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "分片上传成功"})
}
getUploadedChunks 获取已经上传的切片数组
定义json请求结构体
var req struct {
Filename string `json:"filename"`
FileMd5 string `json:"fileMd5"`
TotalChunks int `json:"totalChunks"`
}
从请求中利用gin的ShouldBindJSON方法,把请求中的json解析出来赋到req结构体上
if err := c.ShouldBindJSON(&req); err != nil {
log.Println("Failed to bind JSON request:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
定义上传目录
// 上传目录
uploadDir := "./uploads"
// 已经上传的分片切片
uploadedChunks := []int{}
从req中取出TotalChunks,再遍历totalChunks次,拼接出分片路径,看看这个文件在不在分片目录里面,如果在,就把遍历用的索引i 加到uploadedChunks里面,表示这个分片已经上传过了
for i := 0; i < req.TotalChunks; i++ {
chunkFilename := fmt.Sprintf("%s_%s_%d", req.Filename, req.FileMd5, i)
chunkPath := filepath.Join(uploadDir, chunkFilename)
if _, err := os.Stat(chunkPath); err == nil {
uploadedChunks = append(uploadedChunks, i)
}
}
最后成功响应uploadedChunks,告诉前端已经上传了多少切片了
c.JSON(http.StatusOK, gin.H{"success": true, "uploadedChunks": uploadedChunks})
mergeChunks 合并分片
解析请求:从请求中解析出 Filename、TotalChunks 和 FileMd5 这三个字段。
创建合并目录:如果合并目录不存在,则创建它。
创建合并文件:在合并目录中创建一个新文件,用于存储合并后的内容。
遍历分片文件:根据 TotalChunks 的值,遍历所有分片文件,读取每个分片的内容,并将其写入合并文件中。
删除已合并的分片文件:在合并完成后,删除已合并的分片文件。
返回响应:返回一个 JSON 响应,指示文件合并是否成功。
定义请求结构体
var req struct {
Filename string `json:"filename"`
TotalChunks int `json:"totalChunks"`
FileMd5 string `json:"fileMD5"`
}
从请求中读出数据绑定在req变量上
if err := c.ShouldBindJSON(&req); err != nil {
log.Println("Failed to bind JSON request:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
定义上传目录和合并目录,并创建合并目录
uploadDir := "./uploads"
mergedDir := "./merged"
if err := os.MkdirAll(mergedDir, os.ModePerm); err != nil {
log.Println("Failed to create merged directory:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
从req中取出文件名,拼接出要最终文件的路径,创建这个文件,一会儿从uploads目录里面读分片并写入这个文件
mergedFile := filepath.Join(mergedDir, req.Filename)
out, err := os.Create(mergedFile)
if err != nil {
log.Println("Failed to create merged file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer out.Close()
遍历totalChunks,拼接分片名称,打开分片文件,copy二进制数据到刚刚创建的合并文件中,最后删除已经合并的文件
for i := 0; i < req.TotalChunks; i++ {
chunkFilename := fmt.Sprintf("%s_%s_%d", req.Filename, req.FileMd5, i)
chunkPath := filepath.Join(uploadDir, chunkFilename)
// 读取分片文件
chunk, err := os.Open(chunkPath)
if err != nil {
log.Printf("Failed to open chunk %d: %v\n", i, err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
_, err = io.Copy(out, chunk)
chunk.Close() // 立即关闭文件句柄
if err != nil {
log.Printf("Failed to copy chunk %d: %v\n", i, err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
// 删除已合并的分片文件
if err := os.Remove(chunkPath); err != nil {
log.Printf("Failed to delete chunk %d: %v\n", i, err)
}
}
最后返回成功响应
c.JSON(http.StatusOK, gin.H{"success": true, "message": "文件合并成功"})
完整代码
func mergeChunks(c *gin.Context) {
var req struct {
Filename string `json:"filename"`
TotalChunks int `json:"totalChunks"`
FileMd5 string `json:"fileMD5"`
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Println("Failed to bind JSON request:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
uploadDir := "./uploads"
mergedDir := "./merged"
if err := os.MkdirAll(mergedDir, os.ModePerm); err != nil {
log.Println("Failed to create merged directory:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
mergedFile := filepath.Join(mergedDir, req.Filename)
out, err := os.Create(mergedFile)
if err != nil {
log.Println("Failed to create merged file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer out.Close()
for i := 0; i < req.TotalChunks; i++ {
chunkFilename := fmt.Sprintf("%s_%s_%d", req.Filename, req.FileMd5, i)
chunkPath := filepath.Join(uploadDir, chunkFilename)
// 读取分片文件
chunk, err := os.Open(chunkPath)
if err != nil {
log.Printf("Failed to open chunk %d: %v\n", i, err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
_, err = io.Copy(out, chunk)
chunk.Close() // 立即关闭文件句柄
if err != nil {
log.Printf("Failed to copy chunk %d: %v\n", i, err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
// 删除已合并的分片文件
if err := os.Remove(chunkPath); err != nil {
log.Printf("Failed to delete chunk %d: %v\n", i, err)
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "文件合并成功"})
}
main函数
func main() {
r := gin.Default()
config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
r.Use(cors.New(config))
r.POST("/upload-chunk", uploadChunk)
r.POST("/get-uploaded-chunks", getUploadedChunks)
r.POST("/merge-chunks", mergeChunks)
r.GET("/download/:filename", downloadFile)
log.Println("Server started at :8080")
if err := r.Run(":8080"); err != nil {
log.Fatal("Failed to start server:", err)
}
}
后端全部代码
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func uploadChunk(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
log.Println("Failed to get file from form:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
chunkIndex := c.PostForm("chunkIndex")
totalChunks := c.PostForm("totalChunks")
fileMd5 := c.PostForm("fileMD5")
index, _ := strconv.Atoi(chunkIndex)
index = index + 1
fmt.Println("Received chunk:", index, "of", totalChunks, "for file:", fileMd5)
uploadDir := "./uploads"
if err := os.MkdirAll(uploadDir, os.ModePerm); err != nil {
log.Println("Failed to create upload directory:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
chunkFilename := fmt.Sprintf("%s_%s_%s", file.Filename, fileMd5, chunkIndex)
filePath := filepath.Join(uploadDir, chunkFilename)
// 保存分片文件
dst, err := os.Create(filePath)
if err != nil {
log.Println("Failed to create chunk file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer dst.Close()
src, err := file.Open()
if err != nil {
log.Println("Failed to open uploaded file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer src.Close()
if _, err := io.Copy(dst, src); err != nil {
log.Println("Failed to save chunk file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "分片上传成功"})
}
func getUploadedChunks(c *gin.Context) {
var req struct {
Filename string `json:"filename"`
FileMd5 string `json:"fileMd5"`
TotalChunks int `json:"totalChunks"`
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Println("Failed to bind JSON request:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
uploadDir := "./uploads"
uploadedChunks := []int{}
for i := 0; i < req.TotalChunks; i++ {
chunkFilename := fmt.Sprintf("%s_%s_%d", req.Filename, req.FileMd5, i)
chunkPath := filepath.Join(uploadDir, chunkFilename)
if _, err := os.Stat(chunkPath); err == nil {
uploadedChunks = append(uploadedChunks, i)
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "uploadedChunks": uploadedChunks})
}
func mergeChunks(c *gin.Context) {
var req struct {
Filename string `json:"filename"`
TotalChunks int `json:"totalChunks"`
FileMd5 string `json:"fileMD5"`
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Println("Failed to bind JSON request:", err)
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
uploadDir := "./uploads"
mergedDir := "./merged"
if err := os.MkdirAll(mergedDir, os.ModePerm); err != nil {
log.Println("Failed to create merged directory:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
mergedFile := filepath.Join(mergedDir, req.Filename)
out, err := os.Create(mergedFile)
if err != nil {
log.Println("Failed to create merged file:", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
defer out.Close()
for i := 0; i < req.TotalChunks; i++ {
chunkFilename := fmt.Sprintf("%s_%s_%d", req.Filename, req.FileMd5, i)
chunkPath := filepath.Join(uploadDir, chunkFilename)
// 读取分片文件
chunk, err := os.Open(chunkPath)
if err != nil {
log.Printf("Failed to open chunk %d: %v\n", i, err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
_, err = io.Copy(out, chunk)
chunk.Close() // 立即关闭文件句柄
if err != nil {
log.Printf("Failed to copy chunk %d: %v\n", i, err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
// 删除已合并的分片文件
if err := os.Remove(chunkPath); err != nil {
log.Printf("Failed to delete chunk %d: %v\n", i, err)
}
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "文件合并成功"})
}
func downloadFile(c *gin.Context) {
filename := c.Param("filename")
mergedDir := "./merged"
filePath := filepath.Join(mergedDir, filename)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
log.Println("File not found:", err)
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "文件未找到"})
return
}
c.File(filePath)
}
func main() {
r := gin.Default()
config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"}
r.Use(cors.New(config))
r.POST("/upload-chunk", uploadChunk)
r.POST("/get-uploaded-chunks", getUploadedChunks)
r.POST("/merge-chunks", mergeChunks)
r.GET("/download/:filename", downloadFile)
log.Println("Server started at :8080")
if err := r.Run(":8080"); err != nil {
log.Fatal("Failed to start server:", err)
}
}