使用WebUploader实现图片分片上传
程序员文章站
2024-03-19 23:02:22
...
1、添加js和CSS文件
<link rel="stylesheet" type="text/css" href="/resources/css/WebUploader/webuploader.css" />
<script type="text/javascript" src="/resources/js/jquery-1.7.min.js"></script>
<script type="text/javascript" src="/resources/js/WebUploader/webuploader.js"></script>
<script type="text/javascript" src="/resources/js/WebUploader/md5.js"></script>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
<style type="text/css">
.itemDel, .itemStop, .itemUpload{
margin-left: 15px;
color: blue;
cursor: pointer;
}
#theList{
width: 80%;
min-height: 100px;
border: 1px solid red;
}
#theList .itemStop{
display: none;
}
</style>
2、html代码
<div id="uploader">
<ul id="theList"></ul>
<div id="picker">选择文件</div>
</div>
3、js代码<script type="text/javascript">
var userInfo = {userId:"kazaff", md5:""}; //用户会话信息
var chunkSize = 5 *1024 * 1024; //分块大小
var uniqueFileName = null; //文件唯一标识符
var md5Mark = null;
function getServer(type){ //测试用,根据不同类型的后端返回对应的请求地址
return "<%=basePath%>fileUpload";
}
var backEndUrl = getServer("java");
WebUploader.Uploader.register({
"before-send-file": "beforeSendFile"
, "before-send": "beforeSend"
, "after-send-file": "afterSendFile"
}, {
beforeSendFile: function(file){
//秒传验证
var task = new $.Deferred();
var start = new Date().getTime();
(new WebUploader.Uploader()).md5File(file, 0, 10*1024*1024).progress(function(percentage){
console.log(percentage);
}).then(function(val){
console.log("总耗时: "+((new Date().getTime()) - start)/1000);
md5Mark = val;
userInfo.md5 = val;
$.ajax({
type: "POST"
, url: backEndUrl
, data: {
status: "md5Check"
, md5: val
}
, cache: false
, timeout: 1000 //todo 超时的话,只能认为该文件不曾上传过
, dataType: "json"
}).then(function(data, textStatus, jqXHR){
//console.log(data);
if(data.ifExist){ //若存在,这返回失败给WebUploader,表明该文件不需要上传
task.reject();
uploader.skipFile(file);
file.path = data.path;
UploadComlate(file);
}else{
task.resolve();
//拿到上传文件的唯一名称,用于断点续传
uniqueFileName = md5(''+userInfo.userId+file.name+file.type+file.lastModifiedDate+file.size);
}
}, function(jqXHR, textStatus, errorThrown){ //任何形式的验证失败,都触发重新上传
task.resolve();
//拿到上传文件的唯一名称,用于断点续传
uniqueFileName = md5(''+userInfo.userId+file.name+file.type+file.lastModifiedDate+file.size);
});
});
return $.when(task);
}
, beforeSend: function(block){
//分片验证是否已传过,用于断点续传
var task = new $.Deferred();
$.ajax({
type: "POST"
, url: backEndUrl
, data: {
status: "chunkCheck"
, name: uniqueFileName
, chunkIndex: block.chunk
, size: block.end - block.start
}
, cache: false
, timeout: 1000 //todo 超时的话,只能认为该分片未上传过
, dataType: "json"
}).then(function(data, textStatus, jqXHR){
if(data.ifExist){ //若存在,返回失败给WebUploader,表明该分块不需要上传
task.reject();
}else{
task.resolve();
}
}, function(jqXHR, textStatus, errorThrown){ //任何形式的验证失败,都触发重新上传
task.resolve();
});
return $.when(task);
}
, afterSendFile: function(file){
var chunksTotal = 0;
if((chunksTotal = Math.ceil(file.size/chunkSize)) > 1){
//合并请求
var task = new $.Deferred();
$.ajax({
type: "POST"
, url: backEndUrl
, data: {
status: "chunksMerge"
, name: uniqueFileName
, chunks: chunksTotal
, ext: file.ext
, md5: md5Mark
}
, cache: false
, dataType: "json"
}).then(function(data, textStatus, jqXHR){
//todo 检查响应是否正常
task.resolve();
file.path = data.path;
UploadComlate(file);
}, function(jqXHR, textStatus, errorThrown){
task.reject();
});
return $.when(task);
}else{
UploadComlate(file);
}
}
});
var uploader = WebUploader.create({
auto:true //允许自动上传
, swf: "/js/WebUploaderUploader.swf" //引用swf文件
, server: backEndUrl //后台响应服务器路径
, pick:{
id:"##picker" //点击触发文件上传的按钮id
, multiple:false //是否可选择多个文件(默认为true)
}
, resize: true //不压缩文件
, paste: document.body //指定监听paste事件的容器
, disableGlobalDnd: true //是否禁止拖拽
, thumb: { //配置生成缩略图的选项
width: 200
, height: 200
, quality: 70
, allowMagnify: true
, crop: true
//, type: "image/jpeg"
}
, compress: { //配置上传前压缩的图片的选项
quality: 100 //图片压缩后比例(只允许jpeg格式)
, allowMagnify: false //是否允许放大(设为false,保证不失真)
, crop: false //是否允许裁剪
, preserveHeaders: true //是否保留头部meta信息
, noCompressIfLarger: true //如果压缩后图片比原图要大,是否选择原图
,compressSize: 1024 //小于此值不进行压缩(单位:字节)
}
, accept: {
title: 'Images',
extensions: 'gif,jpg,jpeg,bmp,png',
mimeTypes: 'image/jpg,image/jpeg,image/png,image/bmp'
}
, duplicate: true //支持重复上传
, prepareNextFile: true //是否允许在文件传输时提前把下一个文件准备好
, chunked: true //是否要分片处理
, chunkSize: chunkSize //分片数量,默认为2
, threads: threads //上传并发数,默认为3
, formData: function(){return $.extend(true, {}, userInfo);} //文件上传请求的参数表
, fileNumLimit: 10 //验证文件总数量, 超出则不允许加入队列
, fileSingleSizeLimit: 1000 * 1024 * 1024 //验证文件总大小是否超出限制, 超出则不允许加入队列
});
uploader.on("fileQueued", function(file){
$("#theList").append(
'<li id="'+file.id+'">' +
'<img /><span>'+file.name+'</span><span class="itemUpload">上传</span><span class="itemStop">暂停</span><span class="itemDel">删除</span>' +
'<div class="percentage"></div>' +
'</li>');
var $img = $("#" + file.id).find("img");
uploader.makeThumb(file, function(error, src){
if(error){
$img.replaceWith("<span>不能预览</span>");
}
$img.attr("src", src);
});
});
$("#theList").on("click", ".itemUpload", function(){
uploader.upload();
//"上传"-->"暂停"
$(this).hide();
$(".itemStop").show();
});
$("#theList").on("click", ".itemStop", function(){
uploader.stop(true);
//"暂停"-->"上传"
$(this).hide();
$(".itemUpload").show();
});
//todo 如果要删除的文件正在上传(包括暂停),则需要发送给后端一个请求用来清除服务器端的缓存文件
$("#theList").on("click", ".itemDel", function(){
uploader.removeFile($(this).parent().attr("id")); //从上传文件列表中删除
$(this).parent().remove(); //从上传列表dom中删除
});
uploader.on("uploadProgress", function(file, percentage){
$("#" + file.id + " .percentage").text(percentage * 100 + "%");
});
function UploadComlate(file){
console.log(file);
$("#" + file.id + " .percentage").text("上传完毕");
$(".itemStop").hide();
$(".itemUpload").hide();
$(".itemDel").hide();
}
</script>
4、后台代码
(1)、bean层
package com.bean;
public class FileInfo {
private String md5;
private int chunkIndex;
private String size;
private String name;
private String userId;
private String id;
private int chunks;
private int chunk;
private String lastModifiedDate;
private String type;
private String ext;
public FileInfo(){}
public FileInfo(String name, String size, int chunkIndex){
this.name = name;
this.size = size;
this.chunkIndex = chunkIndex;
}
public FileInfo(String userId, String id){
this.userId = userId;
this.id = id;
}
public FileInfo(String md5){
this.md5 = md5;
}
public FileInfo(int chunks, int chunk, String userId, String id, String name, String size, String lastModifiedDate, String type){
this.userId = userId;
this.id = id;
this.name = name;
this.size = size;
this.chunks = chunks;
this.chunk = chunk;
this.lastModifiedDate = lastModifiedDate;
this.type = type;
}
public FileInfo(String name, int chunks, String ext, String md5){
this.name = name;
this.chunks = chunks;
this.ext = ext;
this.md5 = md5;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(String lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
public int getChunks() {
return chunks;
}
public void setChunks(int chunks) {
this.chunks = chunks;
}
public int getChunk() {
return chunk;
}
public void setChunk(int chunk) {
this.chunk = chunk;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public int getChunkIndex() {
return chunkIndex;
}
public void setChunkIndex(int chunkIndex) {
this.chunkIndex = chunkIndex;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getExt() {
return ext;
}
public void setExt(String ext) {
this.ext = ext;
}
}
(2)、Controller层
package com.controller;
import com.bean.FileInfo;
import com.service.webUploader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@Controller
@RequestMapping("/")
public class FileUploadController {
private final static Logger log = LoggerFactory.getLogger(FileUploadController.class);
@Autowired
private webUploader wu;
@RequestMapping(method = RequestMethod.GET)
public String printWelcome(ModelMap model) {
model.addAttribute("message", "Hello world!");
return "hello";
}
//大文件上传
@RequestMapping(value = "fileUpload", method = RequestMethod.POST)
@ResponseBody
public String fileUpload(String status, FileInfo info, @RequestParam(value = "file", required = false) MultipartFile file){
String uploadFolder="D:\tupian\";
if(status == null){ //文件上传
if(file != null && !file.isEmpty()){ //验证请求不会包含数据上传,所以避免NullPoint这里要检查一下file变量是否为null
try {
File target = wu.getReadySpace(info, this.uploadFolder); //为上传的文件准备好对应的位置
if(target == null){
return "{\"status\": 0, \"message\": \"" + wu.getErrorMsg() + "\"}";
}
file.transferTo(target); //保存上传文件
//将MD5签名和合并后的文件path存入持久层,注意这里这个需求导致需要修改webuploader.js源码3170行
//因为原始webuploader.js不支持为formData设置函数类型参数,这将导致不能在控件初始化后修改该参数
if(info.getChunks() <= 0){
if(!wu.saveMd52FileMap(info.getMd5(), target.getName())){
log.error("文件[" + info.getMd5() + "=>" + target.getName() + "]保存关系到持久成失败,但并不影响文件上传,只会导致日后该文件可能被重复上传而已");
}
}
return "{\"status\": 1, \"path\": \"" + target.getName() + "\"}";
}catch(IOException ex){
log.error("数据上传失败", ex);
return "{\"status\": 0, \"message\": \"数据上传失败\"}";
}
}
}else{
if(status.equals("md5Check")){ //秒传验证
String path = wu.md5Check(info.getMd5());
if(path == null){
return "{\"ifExist\": 0}";
}else{
return "{\"ifExist\": 1, \"path\": \"" + path + "\"}";
}
}else if(status.equals("chunkCheck")){ //分块验证
//检查目标分片是否存在且完整
if(wu.chunkCheck(this.uploadFolder + "/" + info.getName() + "/" + info.getChunkIndex(), Long.valueOf(info.getSize()))){
return "{\"ifExist\": 1}";
}else{
return "{\"ifExist\": 0}";
}
}else if(status.equals("chunksMerge")){ //分块合并
String path = wu.chunksMerge(info.getName(), info.getExt(), info.getChunks(), info.getMd5(), this.uploadFolder);
if(path == null){
return "{\"status\": 0, \"message\": \"" + wu.getErrorMsg() + "\"}";
}
return "{\"status\": 1, \"path\": \"" + path + "\", \"message\": \"中文测试\"}";
}
}
log.error("请求参数不完整");
return "{\"status\": 0, \"message\": \"请求参数不完整\"}";
}
}
(3)、Service层package com.service;
import com.bean.FileInfo;
import com.util.fileLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.locks.Lock;
/**
* Created by kazaff on 2014/12/2.
*/
@Service
@Scope("prototype")
public class webUploader {
private final static Logger log = LoggerFactory.getLogger(webUploader.class);
/**
* 错误详情
*/
private String msg;
/**
* 秒传验证
* 根据文件的MD5签名判断该文件是否已经存在
*
* @param key 文件的md5签名
* @return 若存在则返回该文件的路径,不存在则返回null
*/
public String md5Check(String key){
//todo 模拟去数据库查找
Map<String, String> data = new HashMap<String, String>();
data.put("b0201e4d41b2eeefc7d3d355a44c6f5a", "kazaff2.jpg");
if(data.containsKey(key)){
return data.get(key);
}else{
return null;
}
}
/**
* 分片验证
* 验证对应分片文件是否存在,大小是否吻合
* @param file 分片文件的路径
* @param size 分片文件的大小
* @return
*/
public boolean chunkCheck(String file, Long size){
//检查目标分片是否存在且完整
File target = new File(file);
if(target.isFile() && size == target.length()){
return true;
}else{
return false;
}
}
/**
* 分片合并操作
* 要点:
* > 合并: NIO
* > 并发锁: 避免多线程同时触发合并操作
* > 清理: 合并清理不再需要的分片文件、文件夹、tmp文件
* @param folder 分片文件所在的文件夹名称
* @param ext 合并后的文件后缀名
* @param chunks 分片总数
* @param md5 文件签名
* @param path 合并后的文件所存储的位置
* @return
*/
public String chunksMerge(String folder, String ext, int chunks, String md5, String path){
//合并后的目标文件
String target;
//检查是否满足合并条件:分片数量是否足够
if(chunks == this.getChunksNum(path + "/" + folder)){
//同步指定合并的对象
Lock lock = fileLock.getLock(folder);
lock.lock();
try{
//检查是否满足合并条件:分片数量是否足够
//File[] files = this.getChunks(path + "/" +folder);
List<File> files = new ArrayList<File>(Arrays.asList(this.getChunks(path + "/" +folder)));
if(chunks == files.size()){
//按照名称排序文件,这里分片都是按照数字命名的
Collections.sort(files, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
if(Integer.valueOf(o1.getName()) < Integer.valueOf(o2.getName())){
return -1;
}
return 1;
}
});
//创建合并后的文件
File outputFile = new File(path + "/" + this.randomFileName(ext));
if(outputFile.exists()){
log.error("文件[" + folder + "]随机命名冲突");
this.setErrorMsg("文件随机命名冲突");
return null;
}
outputFile.createNewFile();
FileChannel outChannel = new FileOutputStream(outputFile).getChannel();
//合并
FileChannel inChannel;
for(File file : files){
inChannel = new FileInputStream(file).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
//删除分片
if(!file.delete()){
log.error("分片[" + folder + "=>" + file.getName() + "]删除失败");
}
}
outChannel.close();
files = null;
//将MD5签名和合并后的文件path存入持久层
if(this.saveMd52FileMap(md5, outputFile.getName())){
log.error("文件[" + md5 + "=>" + outputFile.getName() + "]保存关系到持久成失败,但并不影响文件上传,只会导致日后该文件可能被重复上传而已");
}
//清理:文件夹,tmp文件
this.cleanSpace(folder, path);
return outputFile.getName();
}
}catch(Exception ex){
log.error("数据分片合并失败", ex);
this.setErrorMsg("数据分片合并失败");
return null;
}finally {
//解锁
lock.unlock();
//清理锁对象
fileLock.removeLock(folder);
}
}
//去持久层查找对应md5签名,直接返回对应path
target = this.md5Check(md5);
if(target == null){
log.error("文件[签名:" + md5 + "]数据不完整,可能该文件正在合并中");
this.setErrorMsg("数据不完整,可能该文件正在合并中");
return null;
}
return target;
}
/**
* 将MD5签名和目标文件path的映射关系存入持久层
* @param key md5签名
* @param file 文件路径
* @return
*/
public boolean saveMd52FileMap(String key, String file){
//todo
return true;
}
/**
* 为上传的文件创建对应的保存位置
* 若上传的是分片,则会创建对应的文件夹结构和tmp文件
* @param info 上传文件的相关信息
* @param path 文件保存根路径
* @return
*/
public File getReadySpace(FileInfo info, String path){
//创建上传文件所需的文件夹
if(!this.createFileFolder(path, false)){
return null;
}
String newFileName; //上传文件的新名称
//如果是分片上传,则需要为分片创建文件夹
if (info.getChunks() > 0) {
newFileName = String.valueOf(info.getChunk());
String fileFolder = this.md5(info.getUserId() + info.getName() + info.getType() + info.getLastModifiedDate() + info.getSize());
if(fileFolder == null){
return null;
}
path += "/" + fileFolder; //文件上传路径更新为指定文件信息签名后的临时文件夹,用于后期合并
if(!this.createFileFolder(path, true)){
return null;
}
} else {
//生成随机文件名
newFileName = this.randomFileName(info.getName());
}
return new File(path, newFileName);
}
/**
* 清理分片上传的相关数据
* 文件夹,tmp文件
* @param folder 文件夹名称
* @param path 上传文件根路径
* @return
*/
private boolean cleanSpace(String folder, String path){
//删除分片文件夹
File garbage = new File(path + "/" + folder);
if(!garbage.delete()){
return false;
}
//删除tmp文件
garbage = new File(path + "/" + folder + ".tmp");
if(!garbage.delete()){
return false;
}
return true;
}
/**
* 获取指定文件的所有分片
* @param folder 文件夹路径
* @return
*/
private File[] getChunks(String folder){
File targetFolder = new File(folder);
return targetFolder.listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
if (file.isDirectory()) {
return false;
}
return true;
}
});
}
/**
* 获取指定文件的分片数量
* @param folder 文件夹路径
* @return
*/
private int getChunksNum(String folder){
File[] filesList = this.getChunks(folder);
return filesList.length;
}
/**
* 创建存放上传的文件的文件夹
* @param file 文件夹路径
* @return
*/
private boolean createFileFolder(String file, boolean hasTmp){
//创建存放分片文件的临时文件夹
File tmpFile = new File(file);
if(!tmpFile.exists()){
try {
tmpFile.mkdir();
}catch(SecurityException ex){
log.error("无法创建文件夹", ex);
this.setErrorMsg("无法创建文件夹");
return false;
}
}
if(hasTmp){
//创建一个对应的文件,用来记录上传分片文件的修改时间,用于清理长期未完成的垃圾分片
tmpFile = new File(file + ".tmp");
if(tmpFile.exists()){
tmpFile.setLastModified(System.currentTimeMillis());
}else{
try{
tmpFile.createNewFile();
}catch(IOException ex){
log.error("无法创建tmp文件", ex);
this.setErrorMsg("无法创建tmp文件");
return false;
}
}
}
return true;
}
/**
* 为上传的文件生成随机名称
* @param originalName 文件的原始名称,主要用来获取文件的后缀名
* @return
*/
private String randomFileName(String originalName){
String ext[] = originalName.split("\\.");
return UUID.randomUUID().toString() + "." + ext[ext.length-1];
}
/**
* MD5签名
* @param content 要签名的内容
* @return
*/
private String md5(String content){
StringBuffer sb = new StringBuffer();
try{
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(content.getBytes("UTF-8"));
byte[] tmpFolder = md5.digest();
for(int i = 0; i < tmpFolder.length; i++){
sb.append(Integer.toString((tmpFolder[i] & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}catch(NoSuchAlgorithmException ex){
log.error("无法生成文件的MD5签名", ex);
this.setErrorMsg("无法生成文件的MD5签名");
return null;
}catch(UnsupportedEncodingException ex){
log.error("无法生成文件的MD5签名", ex);
this.setErrorMsg("无法生成文件的MD5签名");
return null;
}
}
/**
* 记录异常错误信息
* @param msg 错误详细
*/
private void setErrorMsg(String msg){
this.msg = msg;
}
/**
* 获取错误详细
* @return
*/
public String getErrorMsg(){
return this.msg;
}
}
(4)、util层package com.util;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class fileLock {
private static Map<String, Lock> LOCKS = new HashMap<String, Lock>();
public static synchronized Lock getLock(String key){
if(LOCKS.containsKey(key)){
return LOCKS.get(key);
}else{
Lock one = new ReentrantLock();
LOCKS.put(key, one);
return one;
}
}
public static synchronized void removeLock(String key){
LOCKS.remove(key);
}
}
推荐阅读