Browse Source

增加了断点续传插件版 保留原始版本供学习原理使用

QM303176530 4 years ago
parent
commit
72eb8ec141

+ 87 - 0
server/api/v1/exa_simple_uploader.go

@@ -0,0 +1,87 @@
+package v1
+
+import (
+	"fmt"
+	"gin-vue-admin/global/response"
+	"gin-vue-admin/model"
+	"gin-vue-admin/service"
+	"gin-vue-admin/utils"
+	"github.com/gin-gonic/gin"
+)
+
+// @Tags SimpleUploader
+// @Summary 断点续传插件版示例
+// @Security ApiKeyAuth
+// @accept multipart/form-data
+// @Produce  application/json
+// @Param file formData file true "断点续传插件版示例"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"上传成功"}"
+// @Router /simpleUploader/upload [post]
+func SimpleUploaderUpload(c *gin.Context) {
+	var chunk model.ExaSimpleUploader
+	_, header, err := c.Request.FormFile("file")
+	chunk.Filename = c.PostForm("filename")
+	chunk.ChunkNumber = c.PostForm("chunkNumber")
+	chunk.CurrentChunkSize = c.PostForm("currentChunkSize")
+	chunk.Identifier = c.PostForm("identifier")
+	chunk.TotalSize = c.PostForm("totalSize")
+	chunk.TotalChunks = c.PostForm("totalChunks")
+	var chunkDir = "./chunk/" + chunk.Identifier + "/"
+	hasDir, _ := utils.PathExists(chunkDir)
+	if !hasDir {
+		utils.CreateDir(chunkDir)
+	}
+	chunkPath := chunkDir + chunk.Filename + chunk.ChunkNumber
+	err = c.SaveUploadedFile(header, chunkPath)
+	if err != nil {
+		response.FailWithMessage(fmt.Sprintf("切片创建失败,%v", err), c)
+		return
+	}
+	chunk.CurrentChunkPath = chunkPath
+	err = service.SaveChunk(chunk)
+	if err != nil {
+		response.FailWithMessage(fmt.Sprintf("切片创建失败,%v", err), c)
+		return
+	} else {
+		response.Ok(c)
+	}
+}
+
+// @Tags SimpleUploader
+// @Summary 断点续传插件版示例
+// @Security ApiKeyAuth
+
+// @Produce  application/json
+// @Param params md5 get "测试文件是否已经存在和判断已经上传过的切片"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /simpleUploader/checkFileMd5 [get]
+func CheckFileMd5(c *gin.Context) {
+	md5 := c.Query("md5")
+	err, chunks, isDone := service.CheckFileMd5(md5)
+	if err != nil {
+		response.FailWithMessage(fmt.Sprintf("md5读取失败,%v", err), c)
+	} else {
+		response.OkWithData(gin.H{
+			"chunks": chunks,
+			"isDone": isDone,
+		}, c)
+	}
+}
+
+// @Tags SimpleUploader
+// @Summary 合并文件
+// @Security ApiKeyAuth
+// @Produce  application/json
+// @Param params md5 get "合并文件"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"合并成功"}"
+// @Router /simpleUploader/mergeFileMd5 [get]
+func MergeFileMd5(c *gin.Context) {
+	md5 := c.Query("md5")
+	fileName := c.Query("fileName")
+	err := service.MergeFileMd5(md5, fileName)
+	if err != nil {
+		response.FailWithMessage(fmt.Sprintf("md5读取失败,%v", err), c)
+	} else {
+		response.OkWithData(gin.H{}, c)
+	}
+}

+ 27 - 0
server/db/20200815_update_patch.sql

@@ -0,0 +1,27 @@
+INSERT INTO `casbin_rule` VALUES ('p', '888', '/simpleUploader/upload', 'POST', '', '', '');
+INSERT INTO `casbin_rule` VALUES ('p', '888', '/simpleUploader/checkFileMd5', 'GET', '', '', '');
+INSERT INTO `casbin_rule` VALUES ('p', '888', '/simpleUploader/mergeFileMd5', 'GET', '', '', '');
+
+-- ----------------------------
+-- Table structure for exa_simple_uploaders
+-- ----------------------------
+DROP TABLE IF EXISTS `exa_simple_uploaders`;
+CREATE TABLE `exa_simple_uploaders`  (
+  `chunk_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '当前切片标记',
+  `current_chunk_size` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '当前切片容量',
+  `current_chunk_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '切片本地路径',
+  `total_size` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '总容量',
+  `identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '文件标识(md5)',
+  `filename` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '文件名',
+  `total_chunks` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '切片总数',
+  `is_done` tinyint(1) NULL DEFAULT NULL COMMENT '是否上传完成',
+  `file_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '文件本地路径'
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Compact;
+
+INSERT INTO `sys_apis` VALUES (100, '2020-08-15 12:10:55', '2020-08-15 12:10:55', NULL, NULL, '/simpleUploader/upload', '插件版分片上传', 'simpleUploader', 'POST');
+INSERT INTO `sys_apis` VALUES (101, '2020-08-15 19:53:53', '2020-08-15 19:53:53', NULL, NULL, '/simpleUploader/checkFileMd5', '文件完整度验证', 'simpleUploader', 'GET');
+INSERT INTO `sys_apis` VALUES (102, '2020-08-15 22:28:04', '2020-08-15 22:28:04', NULL, NULL, '/simpleUploader/mergeFileMd5', '上传完成合并文件', 'simpleUploader', 'GET');
+
+INSERT INTO `sys_authority_menus` VALUES ('888', 53);
+
+INSERT INTO `sys_base_menus` VALUES (53, '2020-08-15 11:41:49', '2020-08-15 11:43:15', NULL, 0, 19, 'simpleUploader', 'simpleUploader', 0, 'view/example/simpleUploader/simpleUploader', '断点续传(插件版)', 'upload', NULL, 6, 0, 0);

File diff suppressed because it is too large
+ 29 - 10
server/db/qmplus.sql


+ 1 - 0
server/initialize/db_table.go

@@ -21,6 +21,7 @@ func DBTables() {
 		model.ExaFileUploadAndDownload{},
 		model.ExaFile{},
 		model.ExaFileChunk{},
+		model.ExaSimpleUploader{},
 		model.ExaCustomer{},
 		model.SysOperationRecord{},
 	)

+ 2 - 0
server/initialize/router.go

@@ -29,6 +29,7 @@ func Routers() *gin.Engine {
 	router.InitAuthorityRouter(ApiGroup)             // 注册角色路由
 	router.InitApiRouter(ApiGroup)                   // 注册功能api路由
 	router.InitFileUploadAndDownloadRouter(ApiGroup) // 文件上传下载功能路由
+	router.InitSimpleUploaderRouter(ApiGroup)        // 断点续传(插件版)
 	router.InitWorkflowRouter(ApiGroup)              // 工作流相关路由
 	router.InitCasbinRouter(ApiGroup)                // 权限相关路由
 	router.InitJwtRouter(ApiGroup)                   // jwt相关路由
@@ -38,6 +39,7 @@ func Routers() *gin.Engine {
 	router.InitSysDictionaryDetailRouter(ApiGroup)   // 字典详情管理
 	router.InitSysDictionaryRouter(ApiGroup)         // 字典管理
 	router.InitSysOperationRecordRouter(ApiGroup)    // 操作记录
+
 	global.GVA_LOG.Info("router register success")
 	return Router
 }

+ 13 - 0
server/model/exa_simple_uploader.go

@@ -0,0 +1,13 @@
+package model
+
+type ExaSimpleUploader struct {
+	ChunkNumber      string `json:"chunkNumber" gorm:"comment:'当前切片标记'"`
+	CurrentChunkSize string `json:"currentChunkSize" gorm:"comment:'当前切片容量'"`
+	CurrentChunkPath string `json:"currentChunkPath" gorm:"comment:'切片本地路径'"`
+	TotalSize        string `json:"totalSize" gorm:"comment:'总容量'"`
+	Identifier       string `json:"identifier" gorm:"comment:'文件标识(md5)'"`
+	Filename         string `json:"filename" gorm:"comment:'文件名'"`
+	TotalChunks      string `json:"totalChunks" gorm:"comment:'切片总数'"`
+	IsDone           bool   `json:"isDone" gorm:"comment:'是否上传完成'"`
+	FilePath         string `json:"filePath" gorm:"comment:'文件本地路径'"`
+}

+ 18 - 0
server/router/exa_simple_uploader.go

@@ -0,0 +1,18 @@
+package router
+
+import (
+	"gin-vue-admin/api/v1"
+	"gin-vue-admin/middleware"
+	"github.com/gin-gonic/gin"
+)
+
+func InitSimpleUploaderRouter(Router *gin.RouterGroup) {
+	ApiRouter := Router.Group("simpleUploader").
+		Use(middleware.JWTAuth()).
+		Use(middleware.CasbinHandler())
+	{
+		ApiRouter.POST("upload", v1.SimpleUploaderUpload) // 上传功能
+		ApiRouter.GET("checkFileMd5", v1.CheckFileMd5)    // 文件完整度验证
+		ApiRouter.GET("mergeFileMd5", v1.MergeFileMd5)    // 合并文件
+	}
+}

+ 72 - 0
server/service/exa_simple_uploader.go

@@ -0,0 +1,72 @@
+package service
+
+import (
+	"fmt"
+	"gin-vue-admin/global"
+	"gin-vue-admin/model"
+	"io/ioutil"
+	"os"
+	"strconv"
+)
+
+// 保存文件切片路径
+func SaveChunk(uploader model.ExaSimpleUploader) (err error) {
+	return global.GVA_DB.Create(uploader).Error
+}
+
+// 检查文件是否已经上传过
+func CheckFileMd5(md5 string) (err error, uploads []model.ExaSimpleUploader, isDone bool) {
+	err = global.GVA_DB.Find(&uploads, "identifier = ? AND is_done = ?", md5, false).Error
+	isDone = global.GVA_DB.First(&model.ExaSimpleUploader{}, "identifier = ? AND is_done = ?", md5, true).RecordNotFound()
+	return err, uploads, !isDone
+}
+
+// 合并文件
+func MergeFileMd5(md5 string, fileName string) (err error) {
+	finishDir := "./finish/"
+	dir := "./chunk/" + md5
+	//如果文件上传成功 不做后续操作 通知成功即可
+	notFinish := global.GVA_DB.First(&model.ExaSimpleUploader{}, "identifier = ? AND is_done = ?", md5, true).RecordNotFound()
+	if !notFinish {
+		return nil
+	}
+
+	//打开切片文件夹
+	rd, err := ioutil.ReadDir(dir)
+	_ = 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(dir + "/" + fileName + strconv.Itoa(k+1))
+		_, err = fd.Write(content)
+		if err != nil {
+			_ = os.Remove(finishDir + fileName)
+		}
+	}
+	//关闭文件
+	defer fd.Close()
+
+	if err != nil {
+		return err
+	}
+	//创建事务
+	tx := global.GVA_DB.Begin()
+	//删除切片信息
+	err = tx.Delete(&model.ExaSimpleUploader{}, "identifier = ? AND is_done = ?", md5, false).Error
+	// 添加文件信息
+	err = tx.Create(&model.ExaSimpleUploader{
+		Identifier: md5,
+		IsDone:     true,
+		FilePath:   finishDir + fileName,
+		Filename:   fileName,
+	}).Error
+	if err != nil {
+		fmt.Println(err)
+		tx.Rollback()
+	}
+	tx.Commit()
+	//清除切片
+	err = os.RemoveAll(dir)
+	return
+}

+ 1 - 0
web/package.json

@@ -27,6 +27,7 @@
     "vue-particle-line": "^0.1.4",
     "vue-quill-editor": "^3.0.6",
     "vue-router": "^3.1.3",
+    "vue-simple-uploader": "^0.7.4",
     "vuescroll": "^4.14.4",
     "vuex": "^3.1.1",
     "vuex-persist": "^2.1.0"

+ 36 - 0
web/src/api/simpleUploader.js

@@ -0,0 +1,36 @@
+
+
+import service from '@/utils/request'
+
+// @Tags SimpleUploader
+// @Summary 断点续传插件版示例
+// @Security ApiKeyAuth
+
+// @Produce  application/json
+// @Param params md5 get "测试文件是否已经存在和判断已经上传过的切片"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /simpleUploader/checkFileMd5 [get]
+export const checkFileMd5 = (params) => {
+    return service({
+        url: "/simpleUploader/checkFileMd5",
+        method: 'get',
+        params
+    })
+}
+
+
+// @Tags SimpleUploader
+// @Summary 合并文件
+// @Security ApiKeyAuth
+// @Produce  application/json
+// @Param params md5 get "合并文件"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"合并成功"}"
+// @Router /simpleUploader/mergeFileMd5 [get]
+export const mergeFileMd5 = (params) => {
+    return service({
+        url: "/simpleUploader/mergeFileMd5",
+        method: 'get',
+        params
+    })
+}
+

+ 2 - 0
web/src/main.js

@@ -51,6 +51,8 @@ import {auth} from '@/directive/auth'
 // 按钮权限指令
 auth(Vue)
 
+import uploader from 'vue-simple-uploader'
+Vue.use(uploader)
 
 new Vue({
     render: h => h(App),

+ 163 - 0
web/src/view/example/simpleUploader/simpleUploader.vue

@@ -0,0 +1,163 @@
+<template>
+  <uploader
+    :options="options"
+    :file-status-text="statusText"
+    :autoStart="false"
+    @file-added="fileAdded"
+    @file-progress="onFileProgress"
+    @file-success="onFileSuccess"
+    @file-error="onFileError"
+    class="uploader-example"
+  >
+    <uploader-unsupport></uploader-unsupport>
+    <uploader-drop>
+      <p>拖拽文件至此或点击</p>
+      <uploader-btn>选择文件</uploader-btn>
+    </uploader-drop>
+    <uploader-list></uploader-list>
+  </uploader>
+</template>
+
+<script>
+var notUploadedChunks = []; // 已经上传过的文件chunkNumber数组
+var isUploaded = false; // 文件已经上传成功了
+import { mapGetters } from "vuex";
+import { checkFileMd5,mergeFileMd5 } from "@/api/simpleUploader";
+import SparkMD5 from "spark-md5";
+const path = process.env.VUE_APP_BASE_API;
+export default {
+  name: "simpleUploader",
+  data(){
+    return{
+      md5:""
+    }
+  },
+  computed: {
+    ...mapGetters("user", ["userInfo", "token"]),
+    statusText() {
+      return {
+        success: "成功了",
+        error: "出错了",
+        uploading: "上传中",
+        paused: "暂停中",
+        waiting: "等待中"
+      };
+    },
+    options() {
+      return {
+        target: path + "/simpleUploader/upload",
+        testChunks: false,
+        simultaneousUploads: 5,
+        chunkSize: 2 * 1024 * 1024,
+        headers: {
+          "x-token": this.token,
+          "x-user-id": this.userInfo.ID
+        },
+        checkChunkUploadedByResponse(chunk) {
+          if (isUploaded) {
+            return true; // return true 会忽略当前文件,不会再发送给后台
+          } else {
+              // 根据已经上传过的切片来进行忽略
+              return (
+                notUploadedChunks &&
+                notUploadedChunks.some(
+                  item => item.chunkNumber == chunk.offset + 1
+                )
+              );
+          }
+        }
+      };
+    }
+  },
+  methods: {
+
+    // 上传单个文件
+    fileAdded(file) {
+      this.computeMD5(file); // 生成MD5
+    },
+    // 计算MD5值
+    computeMD5(file) {
+      var that = this;
+      isUploaded = false; // 这个文件是否已经上传成功过
+      notUploadedChunks = []; // 未成功的chunkNumber
+      var fileReader = new FileReader();
+      var md5 = "";
+
+      file.pause();
+
+      fileReader.readAsArrayBuffer(file.file);
+      fileReader.onload = async function(e) {
+        if (file.size != e.target.result.byteLength) {
+          this.error(
+            "Browser reported success but could not read the file until the end."
+          );
+          return false;
+        }
+        md5 = SparkMD5.ArrayBuffer.hash(e.target.result, false);
+        
+        file.uniqueIdentifier = md5;
+        if (md5 != "") {
+          const res = await checkFileMd5({ md5: md5 });
+          if (res.code == 0) {
+            if (res.data.isDone) {
+              // 上传成功过
+              isUploaded = true;
+              that.$message({
+                message: "该文件已经上传成功过了,秒传成功。",
+                type: "success"
+              });
+
+              file.cancel();
+            } else {
+              isUploaded = false;
+              notUploadedChunks = res.data.chunks;
+              if(notUploadedChunks.length){
+                file.resume();
+              }
+            }
+          }
+        }
+
+        
+      };
+      fileReader.onerror = function() {
+        this.error(
+          "generater md5 时FileReader异步读取文件出错了,FileReader onerror was triggered, maybe the browser aborted due to high memory usage."
+        );
+        return false;
+      };
+    },
+    // 上传进度
+    onFileProgress() {},
+    // 上传成功
+    async onFileSuccess(rootFile, file) {
+      await mergeFileMd5({md5:file.uniqueIdentifier,fileName:file.name})
+    },
+    onFileError(rootFile, file, response) {
+      this.$message({
+        message: response,
+        type: "error"
+      });
+    }
+  }
+};
+</script>
+
+<style>
+.uploader-example {
+  width: 880px;
+  padding: 15px;
+  margin: 40px auto 0;
+  font-size: 12px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
+}
+.uploader-example .uploader-btn {
+  margin-right: 4px;
+}
+.uploader-example .uploader-list {
+  max-height: 440px;
+  overflow: auto;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+</style>

Some files were not shown because too many files changed in this diff