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参数

  1. file 字段 文件二进制
  2. chunkIndex 在上传的分片索引
  3. totalChunks 总分片数量
  4. 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)
	}
}


前端实现