为什么要使用分片上传
这个为什么已经是老篇常谈了,主要的原因无非就是文件比较大,一次性上传如果网络中断等情况客户端又得重新上传,而且没法补充上传。
切片上传流程
客户端:
有一个大文件,对这个文件进行切片,依据实际业务进行拆分,每一个文件片说白了就是一个[]byte
保证文件的一致性:
需要对每一个切片进行md5处理生成这个块的md5
提交内容:
参数:
文件id - 这个是当前文件上传的序列,保证整个上传中是对哪一个文件进行的上传操作
文件key - 当前切片的文件md5 服务端会进行二次校验保证当前文件切片一致
文件keys - 主文件所有切片的md5
文件切片内容 - 当前切片内容[]byte
文件名称 - 这个是后端用于确定的文件名称
服务端:
直接粗暴的使用json接收都可以,因为是接收的文件切片内容,file实际是一个[]byte
然后判断文件key是否与接收的file进行的md5处理一致,如果不一致则抛出异常即可
将当前切片数据写入临时文件
判断文件keys是否均已写入临时文件,如果没有直接返回保存成功即可
如果文件keys均已写入临时文件,那么就依次读文件keys进行读临时文件,然后写入到新文件即可
依据实际业务可以删除原临时文件或者也可以直接将临时文件上传到你的文件存储服务器,由它进行合并
源码: 纯go语言编写客户端及服务端
首先我们需要一个请求结构体
package web
import (
"crypto/md5"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
)
type ChunkFileRequest struct {
FileId string `json:"fileId"` // client create uuid
FileName string `json:"fileName"` // file name
FileKeys []string `json:"fileKeys"` // file slice all key md5
FileKey string `json:"fileKey"` // file now key to md5 - if server read the slice to md5 eq key not eq then fail
File []byte `json:"file"` // now file
ctx *gin.Context // ctx
}
func (cf *ChunkFileRequest) BindingForm(c *gin.Context) error {
if err := c.ShouldBind(cf); err != nil {
return err
}
cf.ctx = c
return cf.md5()
}
func (cf *ChunkFileRequest) md5() error {
fmt.Println(cf.FileKey)
hash := fmt.Sprintf("%x", md5.Sum(cf.File))
fmt.Println(hash)
if hash != cf.FileKey {
return errors.New("current file slice key error")
}
return nil
}
func (cf *ChunkFileRequest) SaveUploadedFile(tempPath, path string) (string, error) {
tempFolder := filepath.Join(tempPath, cf.FileId)
_, err := os.Stat(tempFolder)
if os.IsNotExist(err) {
err := os.MkdirAll(tempFolder, os.ModePerm)
if err != nil {
return "", err
}
}
out, err := os.Create(filepath.Join(tempFolder, cf.FileKey))
if err != nil {
return "", err
}
defer out.Close()
if _, err := out.Write(cf.File); err != nil {
return "", err
}
for _, fileKey := range cf.FileKeys {
tempFile := filepath.Join(tempFolder, fileKey)
if _, err := os.Stat(tempFile); err != nil {
return "", nil
}
}
base := filepath.Dir(path)
if _, err := os.Stat(base); err != nil {
if os.IsNotExist(err) {
err := os.MkdirAll(base, os.ModePerm)
if err != nil {
return "", err
}
}
}
file, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0664)
if err != nil {
return "", err
}
defer file.Close()
for _, fileKey := range cf.FileKeys {
tempFile := filepath.Join(tempFolder, fileKey)
bt, err := os.ReadFile(tempFile)
if err != nil {
return "", err
}
file.Write(bt)
}
return tempFolder, nil
}
// param: fileId
// param: fileName
// param: fileKeys the file slice all file key md5
// param: fileKey now file slice key md5
// param: file now slice file
func ChunkFile(c *gin.Context) {
var cf ChunkFileRequest
if err := cf.BindingForm(c); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": "400", "msg": "bad file param", "err": err.Error()})
return
}
tempFolder, err := cf.SaveUploadedFile("./temp", "./uploads/"+cf.FileName)
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"code": "503", "msg": "bad save upload file", "err": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": "200", "msg": "success"})
if tempFolder != "" {
defer func(tempFolder string) {
os.RemoveAll(tempFolder)
}(tempFolder)
}
}
服务端及客户端测试代码:
func TestChunkFileUploadServer(t *testing.T) {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
w := gin.Default()
w.POST("/chunkFile", web.ChunkFile)
srv := &http.Server{
Addr: ":8080",
Handler: w,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
<-ctx.Done()
stop()
log.Println("shutting down gracefully, press Ctrl+C again to force")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown: ", err)
}
log.Println("Server exiting")
}
func TestChunkFileUploadClient(t *testing.T) {
// your client file path
filePath := ""
fileName := filepath.Base(filePath)
fileInfo, err := os.Stat(filePath)
if err != nil {
log.Fatalf("file stat fail: %v\n", err)
return
}
const chunkSize = 1 << (10 * 2) * 30
num := math.Ceil(float64(fileInfo.Size()) / float64(chunkSize))
fi, err := os.OpenFile(filePath, os.O_RDONLY, os.ModePerm)
if err != nil {
log.Fatalf("open file fail: %v\n", err)
return
}
fileKeyMap := make(map[string][]byte, 0)
fileKeys := make([]string, 0)
for i := 1; i <= int(num); i++ {
file := make([]byte, chunkSize)
fi.Seek((int64(i)-1)*chunkSize, 0)
if len(file) > int(fileInfo.Size()-(int64(i)-1)*chunkSize) {
file = make([]byte, fileInfo.Size()-(int64(i)-1)*chunkSize)
}
fi.Read(file)
key := fmt.Sprintf("%x", md5.Sum(file))
fileKeyMap[key] = file
fileKeys = append(fileKeys, key)
}
fileId := uuid.NewString()
for _, key := range fileKeys {
req := web.ChunkFileRequest{
FileId: fileId,
FileName: fileName,
FileKey: key,
FileKeys: fileKeys,
File: fileKeyMap[key],
}
body, _ := json.Marshal(req)
res, err := http.Post("http://127.0.0.1:8080/chunkFile", "application/json", bytes.NewBuffer(body))
if err != nil {
log.Fatalf("http post fail: %v", err)
return
}
defer res.Body.Close()
msg, _ := io.ReadAll(res.Body)
fmt.Println(string(msg))
}
}