Browse Source

断点续传 开发中

pixel 5 years ago
parent
commit
640fb53f94

+ 118 - 0
QMPlusServer/controller/api/exa_breakpoint_continue.go

@@ -0,0 +1,118 @@
+package api
+
+import (
+	"fmt"
+	"gin-vue-admin/controller/servers"
+	"gin-vue-admin/model/dbModel"
+	"github.com/gin-gonic/gin"
+	"io/ioutil"
+	"strconv"
+)
+
+// @Tags ExaFileUploadAndDownload
+// @Summary 断点续传到服务器
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce  application/json
+// @Param file formData file true "断点续传示例"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"上传成功"}"
+// @Router /fileUploadAndDownload/breakpointContinue [post]
+
+func BreakpointContinue(c *gin.Context) {
+	fileMd5 := c.Request.FormValue("fileMd5")
+	fileName := c.Request.FormValue("fileName")
+	chunkMd5 := c.Request.FormValue("chunkMd5")
+	chunkNumber, _ := strconv.Atoi(c.Request.FormValue("chunkNumber"))
+	chunkTotal, _ := strconv.Atoi(c.Request.FormValue("chunkTotal"))
+	_, FileHeader, err := c.Request.FormFile("file")
+	if err != nil {
+		servers.ReportFormat(c, false, fmt.Sprintf("%v", err), gin.H{})
+	} else {
+		f, err := FileHeader.Open()
+		if err != nil {
+			servers.ReportFormat(c, false, fmt.Sprintf("%v", err), gin.H{})
+		} else {
+			cen, _ := ioutil.ReadAll(f)
+			defer f.Close()
+			if flag := servers.CheckMd5(cen, chunkMd5); flag {
+				err, file := new(dbModel.ExaFile).FindOrCreateFile(fileMd5, fileName, chunkTotal)
+				if err != nil {
+					servers.ReportFormat(c, false, fmt.Sprintf("%v", err), gin.H{})
+				} else {
+					err, pathc := servers.BreakPointContinue(cen, fileName, chunkNumber, chunkTotal, fileMd5)
+					if err != nil {
+						servers.ReportFormat(c, false, fmt.Sprintf("%v", err), gin.H{})
+					} else {
+						err = file.CreateFileChunk(pathc, chunkNumber)
+						if err != nil {
+							servers.ReportFormat(c, false, fmt.Sprintf("%v", err), gin.H{})
+						} else {
+							servers.ReportFormat(c, true, "切片创建成功", gin.H{})
+						}
+					}
+				}
+			} else {
+			}
+		}
+	}
+}
+
+// @Tags ExaFileUploadAndDownload
+// @Summary 查找文件
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce  application/json
+// @Param file params file true "查找文件"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查找成功"}"
+// @Router /fileUploadAndDownload/findFile [post]
+func FindFile(c *gin.Context) {
+	fileMd5 := c.Query("fileMd5")
+	fileName := c.Query("fileName")
+	chunkTotal, _ := strconv.Atoi(c.Query("chunkTotal"))
+	err, file := new(dbModel.ExaFile).FindOrCreateFile(fileMd5, fileName, chunkTotal)
+	if err != nil {
+		servers.ReportFormat(c, false, fmt.Sprintf("查找失败:%v", err), gin.H{})
+	} else {
+		servers.ReportFormat(c, true, "查找成功", gin.H{"file": file})
+	}
+}
+
+// @Tags ExaFileUploadAndDownload
+// @Summary 查找文件
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce  application/json
+// @Param file params file true "查找文件"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查找成功"}"
+// @Router /fileUploadAndDownload/findFile [post]
+func BreakpointContinueFinish(c *gin.Context) {
+	fileMd5 := c.Query("fileMd5")
+	fileName := c.Query("fileName")
+	err, filePath := servers.MakeFile(fileName, fileMd5)
+	if err != nil {
+		servers.ReportFormat(c, true, fmt.Sprintf("文件创建失败:%v", err), gin.H{})
+	} else {
+		servers.ReportFormat(c, true, "文件创建成功", gin.H{"filePath": filePath})
+	}
+}
+
+// @Tags ExaFileUploadAndDownload
+// @Summary 删除切片
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce  application/json
+// @Param file params file true "查找文件"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查找成功"}"
+// @Router /fileUploadAndDownload/removeChunk [post]
+func RemoveChunk(c *gin.Context) {
+	fileMd5 := c.Query("fileMd5")
+	fileName := c.Query("fileName")
+	filePath := c.Query("filePath")
+	err := servers.RemoveChunk(fileMd5)
+	err = new(dbModel.ExaFile).DeleteFileChunk(fileMd5, fileName, filePath)
+	if err != nil {
+		servers.ReportFormat(c, true, fmt.Sprintf("缓存切片删除失败:%v", err), gin.H{})
+	} else {
+		servers.ReportFormat(c, true, "缓存切片删除成功", gin.H{})
+	}
+}

+ 75 - 0
QMPlusServer/controller/servers/breakpoint_continue.go

@@ -0,0 +1,75 @@
+package servers
+
+import (
+	"gin-vue-admin/tools"
+	"io/ioutil"
+	"os"
+	"strconv"
+)
+
+// 前端传来文件片与当前片为什么文件的第几片
+// 后端拿到以后比较次分片是否上传 或者是否为不完全片
+// 前端发送每片多大
+// 前端告知是否为最后一片且是否完成
+
+const breakpointDir = "./breakpointDir/"
+const finishDir = "./fileDir/"
+
+func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (error, string) {
+	path := breakpointDir + fileMd5 + "/"
+	err := os.MkdirAll(path, os.ModePerm)
+	if err != nil {
+		return err, path
+	}
+	err, pathc := makeFileContent(content, fileName, path, contentNumber)
+	return err, pathc
+
+}
+
+func CheckMd5(content []byte, chunkMd5 string) (CanUpload bool) {
+	fileMd5 := tools.MD5V(content)
+	if fileMd5 == chunkMd5 {
+		return true // "可以继续上传"
+	} else {
+		return false // "切片不完整,废弃"
+	}
+}
+
+func makeFileContent(content []byte, fileName string, FileDir string, contentNumber int) (error, string) {
+	path := FileDir + fileName + "_" + strconv.Itoa(contentNumber)
+	f, err := os.Create(path)
+	defer f.Close()
+	if err != nil {
+		return err, path
+	} else {
+		_, err = f.Write(content)
+		if err != nil {
+			return err, path
+		}
+	}
+	return nil, path
+}
+
+func MakeFile(fileName string, FileMd5 string) (error, string) {
+	rd, err := ioutil.ReadDir(breakpointDir + FileMd5)
+	if err != nil {
+		return err, finishDir + fileName
+	}
+	_ = os.MkdirAll(finishDir, os.ModePerm)
+	fd, _ := os.OpenFile(finishDir+fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
+	for k, _ := range rd {
+		content, _ := ioutil.ReadFile(breakpointDir + FileMd5 + "/" + fileName + "_" + strconv.Itoa(k))
+		_, err = fd.Write(content)
+		if err != nil {
+			_ = os.Remove(finishDir + fileName)
+			return err, finishDir + fileName
+		}
+	}
+	defer fd.Close()
+	return nil, finishDir + fileName
+}
+
+func RemoveChunk(FileMd5 string) error {
+	err := os.RemoveAll(breakpointDir + FileMd5)
+	return err
+}

+ 2 - 1
QMPlusServer/init/initRouter/init_router.go

@@ -14,6 +14,7 @@ func InitRouter() *gin.Engine {
 	var Router = gin.Default()
 	//Router.Use(middleware.LoadTls())  // 打开就能玩https了
 	Router.Use(middleware.Logger()) // 如果不需要日志 请关闭这里
+	Router.Use(middleware.Cors())   // 跨域
 	Router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
 	ApiGroup := Router.Group("") // 方便统一添加路由组前缀 多服务器上线使用
 	//Router.Use(middleware.Logger())
@@ -26,6 +27,6 @@ func InitRouter() *gin.Engine {
 	router.InitWorkflowRouter(ApiGroup)              // 工作流相关路由
 	router.InitCasbinRouter(ApiGroup)                // 权限相关路由
 	router.InitJwtRouter(ApiGroup)                   // jwt相关路由
-	router.InitSystemRouter(ApiGroup)				// system相关路由
+	router.InitSystemRouter(ApiGroup)                // system相关路由
 	return Router
 }

+ 3 - 1
QMPlusServer/init/registTable/regist_table.go

@@ -14,8 +14,10 @@ func RegistTable(db *gorm.DB) {
 		sysModel.SysApi{},
 		sysModel.SysBaseMenu{},
 		sysModel.JwtBlacklist{},
-		dbModel.ExaFileUploadAndDownload{},
 		sysModel.SysWorkflow{},
 		sysModel.SysWorkflowStepInfo{},
+		dbModel.ExaFileUploadAndDownload{},
+		dbModel.ExaFile{},
+		dbModel.ExaFileChunk{},
 	)
 }

+ 25 - 0
QMPlusServer/middleware/cors.go

@@ -0,0 +1,25 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+// 处理跨域请求,支持options访问
+func Cors() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		method := c.Request.Method
+		c.Header("Access-Control-Allow-Origin", "*")
+		c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
+		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
+		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
+		c.Header("Access-Control-Allow-Credentials", "true")
+
+		//放行所有OPTIONS方法
+		if method == "OPTIONS" {
+			c.AbortWithStatus(http.StatusNoContent)
+		}
+		// 处理请求
+		c.Next()
+	}
+}

+ 68 - 0
QMPlusServer/model/dbModel/exa_breakpoint_continue.go

@@ -0,0 +1,68 @@
+package dbModel
+
+import (
+	"gin-vue-admin/init/qmsql"
+	"github.com/jinzhu/gorm"
+)
+
+type ExaFile struct {
+	gorm.Model
+	FileName     string
+	FileMd5      string
+	FilePath     string
+	ExaFileChunk []ExaFileChunk
+	ChunkTotal   int
+	IsFinish     bool
+}
+
+type ExaFileChunk struct {
+	gorm.Model
+	ExaFileId       uint
+	FileChunkNumber int
+	FileChunkPath   string
+}
+
+func (f *ExaFile) FileCreateComplete(FileMd5 string, FileName string, FilePath string) error {
+	var file ExaFile
+	upDateFile := make(map[string]interface{})
+	upDateFile["FilePath"] = FilePath
+	upDateFile["IsFinish"] = true
+	err := qmsql.DEFAULTDB.Where("file_md5 = ? AND file_name = ?", FileMd5, FileName).First(&file).Updates(upDateFile).Error
+	return err
+}
+
+func (f *ExaFile) FindOrCreateFile(FileMd5 string, FileName string, ChunkTotal int) (err error, file ExaFile) {
+	var cfile ExaFile
+	cfile.FileMd5 = FileMd5
+	cfile.FileName = FileName
+	cfile.ChunkTotal = ChunkTotal
+	notHaveSameMd5Finish := qmsql.DEFAULTDB.Where("file_md5 = ? AND is_finish = ?", FileMd5, true).First(&file).RecordNotFound()
+	if notHaveSameMd5Finish {
+		err = qmsql.DEFAULTDB.Where("file_md5 = ? AND file_name = ?", FileMd5, FileName).Preload("ExaFileChunk").FirstOrCreate(&file, cfile).Error
+		return err, file
+	} else {
+		cfile.IsFinish = true
+		cfile.FilePath = file.FilePath
+		err = qmsql.DEFAULTDB.Create(&cfile).Error
+		return err, cfile
+	}
+}
+
+// 创建文件切片记录
+func (f *ExaFile) CreateFileChunk(FileChunkPath string, FileChunkNumber int) error {
+	var chunk ExaFileChunk
+	chunk.FileChunkPath = FileChunkPath
+	chunk.ExaFileId = f.ID
+	chunk.FileChunkNumber = FileChunkNumber
+	err := qmsql.DEFAULTDB.Create(&chunk).Error
+	return err
+}
+
+// 删除文件切片记录
+func (f *ExaFile) DeleteFileChunk(fileMd5 string, fileName string, filePath string) error {
+	var chunks []ExaFileChunk
+	var file ExaFile
+	err := qmsql.DEFAULTDB.Where("file_md5 = ? AND file_name = ?", fileMd5, fileName).First(&file).Update("IsFinish", true).Update("file_path", filePath).Error
+	err = qmsql.DEFAULTDB.Where("exa_file_id = ?", file.ID).Delete(&chunks).Unscoped().Error
+	return err
+}

+ 4 - 4
QMPlusServer/model/sysModel/sys_user.go

@@ -35,7 +35,7 @@ func (u *SysUser) Regist() (err error, userInter *SysUser) {
 		return errors.New("用户名已注册"), nil
 	} else {
 		// 否则 附加uuid 密码md5简单加密 注册
-		u.Password = tools.MD5V(u.Password)
+		u.Password = tools.MD5V([]byte(u.Password))
 		u.UUID = uuid.NewV4()
 		err = qmsql.DEFAULTDB.Create(u).Error
 	}
@@ -46,8 +46,8 @@ func (u *SysUser) Regist() (err error, userInter *SysUser) {
 func (u *SysUser) ChangePassword(newPassword string) (err error, userInter *SysUser) {
 	var user SysUser
 	//后期修改jwt+password模式
-	u.Password = tools.MD5V(u.Password)
-	err = qmsql.DEFAULTDB.Where("username = ? AND password = ?", u.Username, u.Password).First(&user).Update("password", tools.MD5V(newPassword)).Error
+	u.Password = tools.MD5V([]byte(u.Password))
+	err = qmsql.DEFAULTDB.Where("username = ? AND password = ?", u.Username, u.Password).First(&user).Update("password", tools.MD5V([]byte(newPassword))).Error
 	return err, u
 }
 
@@ -60,7 +60,7 @@ func (u *SysUser) SetUserAuthority(uuid uuid.UUID, AuthorityId string) (err erro
 //用户登录
 func (u *SysUser) Login() (err error, userInter *SysUser) {
 	var user SysUser
-	u.Password = tools.MD5V(u.Password)
+	u.Password = tools.MD5V([]byte(u.Password))
 	err = qmsql.DEFAULTDB.Where("username = ? AND password = ?", u.Username, u.Password).First(&user).Error
 	if err != nil {
 		return err, &user

+ 9 - 6
QMPlusServer/router/exp_fileUploadAndDownload.go

@@ -2,16 +2,19 @@ package router
 
 import (
 	"gin-vue-admin/controller/api"
-	"gin-vue-admin/middleware"
 	"github.com/gin-gonic/gin"
 )
 
 func InitFileUploadAndDownloadRouter(Router *gin.RouterGroup) {
-	FileUploadAndDownloadGroup := Router.Group("fileUploadAndDownload").Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
-	//.Use(middleware.JWTAuth())
+	FileUploadAndDownloadGroup := Router.Group("fileUploadAndDownload")
+	//.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
 	{
-		FileUploadAndDownloadGroup.POST("/upload", api.UploadFile)       // 上传文件
-		FileUploadAndDownloadGroup.POST("/getFileList", api.GetFileList) // 获取上传文件列表
-		FileUploadAndDownloadGroup.POST("/deleteFile", api.DeleteFile)   // 删除指定文件
+		FileUploadAndDownloadGroup.POST("/upload", api.UploadFile)                                 // 上传文件
+		FileUploadAndDownloadGroup.POST("/getFileList", api.GetFileList)                           // 获取上传文件列表
+		FileUploadAndDownloadGroup.POST("/deleteFile", api.DeleteFile)                             // 删除指定文件
+		FileUploadAndDownloadGroup.POST("/breakpointContinue", api.BreakpointContinue)             // 断点续传
+		FileUploadAndDownloadGroup.GET("/findFile", api.FindFile)                                  // 查询当前文件成功的切片
+		FileUploadAndDownloadGroup.POST("/breakpointContinueFinish", api.BreakpointContinueFinish) // 查询当前文件成功的切片
+		FileUploadAndDownloadGroup.POST("/removeChunk", api.RemoveChunk)                           // 查询当前文件成功的切片
 	}
 }

+ 2 - 2
QMPlusServer/tools/md5.go

@@ -5,8 +5,8 @@ import (
 	"encoding/hex"
 )
 
-func MD5V(str string) string {
+func MD5V(str []byte) string {
 	h := md5.New()
-	h.Write([]byte(str))
+	h.Write(str)
 	return hex.EncodeToString(h.Sum(nil))
 }