1. 上傳頁面代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webuploader文件上傳</title>
<!--引入CSS-->
<link rel="stylesheet" type="text/css" href="./webuploader/webuploader.css">
<!--引入JS-->
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<script type="text/javascript" src="./webuploader/webuploader.js"></script>
</head>
<body>
<div id="uploader" class="wu-example">
<!--用來存放文件信息-->
<div id="thelist" class="uploader-list"></div>
<div class="btns">
<div id="picker">選擇文件</div>
<button id="ctlBtn" class="btn btn-default" onclick="upload()">開始上傳</button>
</div>
</div>
</body>
<script>
//
WebUploader.Uploader.register({
"before-send-file": "beforeSendFile",
"before-send": "beforeSend",
"after-send-file": "afterSendFile"
}, {
//在上傳文件前執行,檢查待文件是否存在
beforeSendFile: function (file) {
// 創建一個deffered,用於通知是否完成操作
var deferred = WebUploader.Deferred();
// 計算文件的唯一標識,用於斷點續傳
(new WebUploader.Uploader()).md5File(file)
.then(function (val) {
this.fileMd5 = val;
this.uploadFile = file;
// alert(this.fileMd5 )
//向服務端請求注冊上傳文件
$.ajax(
{
type: "POST",
url: "http://127.0.0.1:31400/media/upload/register",
data: {
// 文件唯一表示
fileMd5: this.fileMd5,
fileName: file.name,
fileSize: file.size,
mimetype: file.type,
fileExt: file.ext
},
dataType: "json",
success: function (response) {
if (response.success) {
//alert('上傳文件注冊成功開始上傳');
deferred.resolve();
} else {
alert(response.message);
deferred.reject();
}
}
}
);
}.bind(this));
return deferred.promise();
}.bind(this),
//在上傳分片前執行,檢查分片是否已經存在
beforeSend: function (block) {
var deferred = WebUploader.Deferred();
// 每次上傳分塊前校驗分塊,如果已存在分塊則不再上傳,達到斷點續傳的目的
$.ajax(
{
type: "POST",
url: "http://127.0.0.1:31400/media/upload/checkchunk",
data: {
// 文件唯一表示
fileMd5: this.fileMd5,
// 當前分塊下標
chunk: block.chunk,
// 當前分塊大小
chunkSize: block.end - block.start
},
dataType: "json",
success: function (response) {
if (response.fileExist) {
// 分塊存在,跳過該分塊
deferred.reject();
} else {
// 分塊不存在或不完整,重新發送
deferred.resolve();
}
}
}
);
//構建fileMd5參數,上傳分塊時帶上fileMd5
this.uploader.options.formData.fileMd5 = this.fileMd5;
this.uploader.options.formData.chunk = block.chunk;
return deferred.promise();
}.bind(this),
// 在上傳分片完成后觸發,用於后台處理合成分片,判斷上傳文件是否成功以及可以攜帶返回上傳文件成功的url
afterSendFile: function (file) {
// 合並分塊
$.ajax(
{
type: "POST",
url: "http://127.0.0.1:31400/media/upload/mergechunks",
data: {
fileMd5: this.fileMd5,
fileName: file.name,
fileSize: file.size,
mimetype: file.type,
fileExt: file.ext
},
success: function (response) {
//在這里解析合並成功結果
if (response && response.success) {
alert("上傳成功")
} else {
alert("上傳失敗")
}
}
}
);
}.bind(this)
}
);
//創建webuploader實例
var uploader = WebUploader.create(
{
swf: "./webuploader/Uploader.swf",//上傳文件的flash文件,瀏覽器不支持h5時啟動flash
server: "http://127.0.0.1:31400/media/upload/uploadchunk",//上傳分塊的服務端地址,注意跨域問題
fileVal: "file",//文件上傳域的name
pick: "#picker",//指定選擇文件的按鈕容器
auto: false,//手動觸發上傳
disableGlobalDnd: true,//禁掉整個頁面的拖拽功能
chunked: true,// 是否分塊上傳
chunkSize: 1 * 1024 * 1024, // 分塊大小(默認5M)
threads: 3, // 開啟多個線程(默認3個)
prepareNextFile: true// 允許在文件傳輸時提前把下一個文件准備好
}
);
// 將文件添加到隊列
uploader.on("fileQueued", function (file) {
this.uploadFile = file;
this.percentage = 0;
}.bind(this)
);
//選擇文件后觸發
uploader.on("beforeFileQueued", function (file) {
// this.uploader.removeFile(file)
//重置uploader
this.uploader.reset()
this.percentage = 0;
}.bind(this));
// 監控上傳進度
// percentage:代表上傳文件的百分比
uploader.on("uploadProgress", function (file, percentage) {
this.percentage = Math.ceil(percentage * 100);
console.log(percentage)
}.bind(this));
//上傳失敗觸發
uploader.on("uploadError", function (file, reason) {
console.log(reason)
alert("上傳文件失敗");
});
//上傳成功觸發
uploader.on("uploadSuccess", function (file, response) {
console.log(response)
// alert("上傳文件成功!");
});
//每個分塊上傳請求后觸發
uploader.on('uploadAccept', function (file, response) {
if (!(response && response.success)) {//分塊上傳失敗,返回false
return false;
}
});
//觸發執行上傳
function upload(){
if(this.uploadFile && this.uploadFile.id){
this.uploader.upload(this.uploadFile.id);
}else{
alert("請選擇文件");
}
}
</script>
</html>
2. nginx配置
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name video.xuecheng.com;
location / {
alias D:/test/webuploader-demo/;
}
}
}
3. 后台主要代碼
3.1 application.yml
server:
port: 31400
spring:
application:
name: xc-service-manage-media
data:
mongodb:
uri: mongodb://root:root@localhost:27017
database: xc_media
xc-service-manage-media:
upload-location: D:/test/video/
3.2 跨域處理
package com.xuecheng.manage_media.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @author john
* @date 2020/1/1 - 15:07
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允許的域,不要寫*,否則cookie就無法使用了
config.addAllowedOrigin("http://video.xuecheng.com");
//2) 是否發送Cookie信息
config.setAllowCredentials(true);
//3) 允許的請求方式
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
// 4)允許的頭信息
config.addAllowedHeader("*");
//2.添加映射路徑,我們攔截一切請求
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
3.3 控制器代碼
package com.xuecheng.manage_media.controller;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.service.MediaUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* @author john
* @date 2020/1/1 - 13:13
*/
@RestController
@RequestMapping("/media/upload")
public class MediaUploadController {
@Autowired
MediaUploadService mediaUploadService;
//檢查文件是否已經存在
@PostMapping("/register")
public ResponseResult register(@RequestParam("fileMd5") String fileMd5,
@RequestParam("fileName") String fileName,
@RequestParam("fileSize") Long fileSize,
@RequestParam("mimetype") String mimetype,
@RequestParam("fileExt") String fileExt) {
return mediaUploadService.register(fileMd5, fileName, fileSize, mimetype, fileExt);
}
//校驗文件塊
@PostMapping("/checkchunk")
public CheckChunkResult checkchunk(@RequestParam("fileMd5") String fileMd5,
@RequestParam("chunk") Integer chunk,
@RequestParam("chunkSize") Integer chunkSize) {
return mediaUploadService.checkChunk(fileMd5, chunk, chunkSize);
}
//上傳文件塊
@PostMapping("/uploadchunk")
public ResponseResult uploadchunk(@RequestParam("file") MultipartFile file,
@RequestParam("fileMd5") String fileMd5,
@RequestParam("chunk") Integer chunk) {
return mediaUploadService.uploadChunk(file, fileMd5, chunk);
}
//合並文件塊
@PostMapping("/mergechunks")
public ResponseResult mergechunks(@RequestParam("fileMd5") String fileMd5,
@RequestParam("fileName") String fileName,
@RequestParam("fileSize") Long fileSize,
@RequestParam("mimetype") String mimetype,
@RequestParam("fileExt") String fileExt) {
return mediaUploadService.mergeChunks(fileMd5, fileName, fileSize, mimetype, fileExt);
}
}
3.4 service代碼
package com.xuecheng.manage_media.service;
import com.xuecheng.framework.domain.media.MediaFile;
import com.xuecheng.framework.domain.media.response.CheckChunkResult;
import com.xuecheng.framework.domain.media.response.MediaCode;
import com.xuecheng.framework.exception.ExceptionCast;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.manage_media.dao.MediaFileRepository;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.*;
/**
* @author john
* @date 2020/1/1 - 12:07
*/
@Service
@Slf4j
public class MediaUploadService {
@Autowired
MediaFileRepository mediaFileRepository;
//上傳文件的根目錄
@Value("${xc-service-manage-media.upload-location}")
String uploadPath;
/**
* 根據文件md5得到文件路徑
* 規則:
* 一級目錄:md5的第一個字符
* 二級目錄:md5的第二個字符
* 三級目錄:md5
* 文件名:md5+文件擴展名
*
* @param fileMd5 文件md5值
* @param fileExt 文件擴展名
* @return 文件路徑
*/
private String getFilePath(String fileMd5, String fileExt) {
return getFileFolderPath(fileMd5) + fileMd5 + "." + fileExt;
}
//得到文件目錄相對路徑,路徑中去掉根目錄
private String getFileFolderRelativePath(String fileMd5) {
return fileMd5.substring(0, 1) + "/"
+ fileMd5.substring(1, 2) + "/"
+ fileMd5 + "/";
}
//得到文件所在目錄
private String getFileFolderPath(String fileMd5) {
return uploadPath + getFileFolderRelativePath(fileMd5);
}
//創建文件目錄
private boolean createFileFold(String fileMd5) {
//創建上傳文件,目錄
String fileFolderPath = getFileFolderPath(fileMd5);
File fileFolder = new File(fileFolderPath);
if (!fileFolder.exists()) {
//創建文件夾
return fileFolder.mkdirs();
}
return true;
}
//檢查待上傳文件是否存在
public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
// 檢查文件是否上傳
// 1. 獲取文件的路徑
String filePath = getFilePath(fileMd5, fileExt);
File file = new File(filePath);
// 2. 查詢數據庫文件是否存在
Optional<MediaFile> optional = mediaFileRepository.findById(fileMd5);
// 文件存在直接返回
if (file.exists() && optional.isPresent()) {
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
}
//下面說明文件並未上傳
boolean fileFold = createFileFold(fileMd5);
if (!fileFold) {
//上傳文件目錄創建失敗
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_CREATEFOLDER_FAIL);
}
return new ResponseResult(CommonCode.SUCCESS);
}
// 得到塊文件所在目錄
private String getChunkFileFolderPath(String fileMd5) {
return getFileFolderPath(fileMd5) + "/" + "chunks" + "/";
}
//檢查塊文件
public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
//得到塊文件所在路徑
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
//塊文件的文件名稱以1,2,3..序號命名,沒有擴展名
File chunkFile = new File(chunkFileFolderPath + chunk);
if (chunkFile.exists()) {
return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, true);
} else {
return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, false);
}
}
private boolean createChunkFileFolder(String fileMd5) {
//創建上傳文件目錄
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
File chunkFileFolder = new File(chunkFileFolderPath);
if (!chunkFileFolder.exists()) {
//創建文件夾
return chunkFileFolder.mkdirs();
}
return true;
}
//塊文件上傳
public ResponseResult uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {
if (file == null) {
ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_ISNULL);
}
//創建塊文件目錄
boolean fileFold = createChunkFileFolder(fileMd5);
//塊文件
File chunkfile = new File(getChunkFileFolderPath(fileMd5) + chunk);
//上傳的塊文件
InputStream inputStream = null;
FileOutputStream outputStream = null;
try {
inputStream = file.getInputStream();
outputStream = new FileOutputStream(chunkfile);
IOUtils.copy(inputStream, outputStream);
} catch (Exception e) {
e.printStackTrace();
log.error("upload chunk file fail:{}", e.getMessage());
ExceptionCast.cast(MediaCode.CHUNK_FILE_UPLOAD_FAIL);
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return new ResponseResult(CommonCode.SUCCESS);
}
//合並塊文件
public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String
mimeType, String fileExt) {
//獲取塊文件的路徑
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
File chunkFileFolder = new File(chunkFileFolderPath);
if (!chunkFileFolder.exists()) {
chunkFileFolder.mkdirs();
}
//合並文件路徑
File mergeFile = new File(getFilePath(fileMd5, fileExt));
//創建合並文件
//合並文件存在先刪除再創建
if (mergeFile.exists()) {
mergeFile.delete();
}
boolean newFile = false;
try {
newFile = mergeFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
log.error("mergechunks..create mergeFile fail:{}", e.getMessage());
}
if (!newFile) {
ExceptionCast.cast(MediaCode.MERGE_FILE_CREATEFAIL);
}
//獲取塊文件,此列表是已經排好序的列表
List<File> chunkFiles = getChunkFiles(chunkFileFolder);
//合並文件
mergeFile = mergeFile(mergeFile, chunkFiles);
if (mergeFile == null) {
ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
}
//校驗文件
boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
if (!checkResult) {
ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
}
//將文件信息保存到數據庫
MediaFile mediaFile = new MediaFile();
mediaFile.setFileId(fileMd5);
mediaFile.setFileName(fileMd5 + "." + fileExt);
mediaFile.setFileOriginalName(fileName);
//文件路徑保存相對路徑
mediaFile.setFilePath(getFileFolderRelativePath(fileMd5));
mediaFile.setFileSize(fileSize);
mediaFile.setUploadTime(new Date());
mediaFile.setMimeType(mimeType);
mediaFile.setFileType(fileExt);
//狀態為上傳成功
mediaFile.setFileStatus("301002");
MediaFile save = mediaFileRepository.save(mediaFile);
return new ResponseResult(CommonCode.SUCCESS);
}
//校驗文件的md5值
private boolean checkFileMd5(File mergeFile, String md5) {
if (mergeFile == null || StringUtils.isEmpty(md5)) {
return false;
}
//進行md5校驗
FileInputStream mergeFileInputstream = null;
try {
mergeFileInputstream = new FileInputStream(mergeFile);
//得到文件的md5
String mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputstream);
//比較md5
if (md5.equalsIgnoreCase(mergeFileMd5)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
log.error("checkFileMd5 error,file is:{},md5 is: {} ", mergeFile.getAbsoluteFile(), md5);
} finally {
try {
mergeFileInputstream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
//獲取所有塊文件
private List<File> getChunkFiles(File chunkFileFolder) {
//獲取路徑下的所有塊文件
File[] chunkFiles = chunkFileFolder.listFiles();
//將文件數組轉成list,並排序
List<File> chunkFileList = new ArrayList<File>();
chunkFileList.addAll(Arrays.asList(chunkFiles));
//排序
Collections.sort(chunkFileList, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {
return 1;
}
return -1;
}
});
return chunkFileList;
}
//合並文件
private File mergeFile(File mergeFile, List<File> chunkFiles) {
try {
//創建寫文件對象
RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
//遍歷分塊文件開始合並
//讀取文件緩沖區
byte[] b = new byte[1024];
for (File chunkFile : chunkFiles) {
RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r");
int len = -1;
//讀取分塊文件
while ((len = raf_read.read(b)) != -1) {
//向合並文件中寫數據
raf_write.write(b, 0, len);
}
raf_read.close();
}
raf_write.close();
} catch (Exception e) {
e.printStackTrace();
log.error("merge file error:{}", e.getMessage());
return null;
}
return mergeFile;
}
}
4. 執行測試