知启蒙大文件上传组件,该组件实现大文件分片上传,包含服务端接收碎片上传和管理功能,以及前端UI的进度条、上传速度等界面。同时还支持按文件内容MD5码、相同文件名称比对秒传等功能,是业务系统应用不可缺少的组件之一。
ZulUploader.java11KB
/*
* 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙,邂逅框架梦,本文采用木兰宽松许可证第2版]
*
* https://zhiqim.org/project/zhiqim_components/zhiqim_upload_large.htm
*
* Zhiqim UploadLarge is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package org.zhiqim.uploadlarge;
import java.io.File;
import java.io.RandomAccessFile;
import org.zhiqim.kernel.model.results.RE;
import org.zhiqim.kernel.util.Files;
import org.zhiqim.kernel.util.Ids;
import org.zhiqim.kernel.util.Sqls;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.Validates;
import org.zhiqim.orm.ORM;
import org.zhiqim.orm.dbo.Selector;
import org.zhiqim.orm.dbo.Updater;
import org.zhiqim.uploadlarge.dbo.UpllChunk;
import org.zhiqim.uploadlarge.dbo.UpllFile;
/**
* 大文件上传处理器接口
*
* @version v1.0.0 @author zouzhigang 2015-10-23 新建与整理
*/
public class ZulUploader implements ZulConstants
{
/**
* 通过文件编号读取文件路径
*
* @param fileId 文件编号
* @return 上传文件结果集
*/
public static RE<UpllFile> item(String fileId)
{
return ORM.tabler().item(UpllFile.class, fileId);
}
/**
* 通过文件编号读取文件路径
*
* @param fileId 文件编号
* @return 上传文件
* @throws Exception 可能的异常
*/
public static UpllFile getFile(String fileId) throws Exception
{
return ORM.table().item(UpllFile.class, fileId);
}
/**
* 通过文件MD5码查询是否有已上传的文件
*
* @param fileMd5 文件MD5码
* @return 上传文件
* @throws Exception 可能的异常
*/
public static UpllFile queryFile(String fileMd5) throws Exception
{
return ORM.table().item(UpllFile.class, new Selector("fileMd5", fileMd5));
}
/**
* 通过文件MD5码和碎片大小查询是否有正在上传的文件
*
* @param fileMd5 文件MD5码
* @param chunkSize 碎片大小
* @return 碎片编号,存在则返回chunkNo,不存在返回-1
* @throws Exception 可能的异常
*/
public static UpllChunk queryChunk(String fileMd5, int chunkSize) throws Exception
{
return ORM.table().item(UpllChunk.class, fileMd5, chunkSize);
}
/**
* 保存碎片数据
*
* @param data 碎片数据,第一个或中间的碎片
* @param fileDir 文件目录,包含根目录
* @param fileName 文件名,这里可能有中文名
* @param fileMd5 文件MD5码
* @param fileLength 文件总长度
* @param chunkSize 碎片大小
* @param chunkNum 碎片总数
* @param chunkNo 碎片编号
* @throws Exception 可能的异常
*/
public static UpllChunk createChunkData(byte[] data, String fileDir, String fileName, String fileMd5, long fileLength, int chunkSize, int chunkNum) throws Exception
{
String fileTempPath = getFileChunkPath(fileDir, fileMd5);
fileName = Files.getFileName(fileName);
String fileExt = Files.getFileExt(fileName);
//写入到文件
RandomAccessFile file = new RandomAccessFile(new File(fileTempPath), _RW_);
file.setLength(fileLength);
file.write(data);
file.close();
//插入到数据表
UpllChunk chunk = new UpllChunk();
chunk.setChunkNo(1);
chunk.setChunkNum(chunkNum);
chunk.setChunkSize(chunkSize);
chunk.setChunkTime(Sqls.nowTimestamp());
chunk.setFileExt(fileExt);
chunk.setFileMd5(fileMd5);
chunk.setFileName(fileName);
chunk.setFileTempPath(fileTempPath);
ORM.table().replace(chunk);
return chunk;
}
/**
* 保存碎片数据
*
* @param data 碎片数据,第一个或中间的碎片
* @param chunk 碎片对象
* @param chunkNo 碎片编号
* @throws Exception 可能的异常
*/
public static void saveChunkData(byte[] data, UpllChunk chunk, int chunkNo) throws Exception
{
String fileTempPath = chunk.getFileTempPath();
//跳转到碎片位置,写入到文件
RandomAccessFile file = new RandomAccessFile(new File(fileTempPath), _RW_);
file.seek((chunkNo-1)*chunk.getChunkSize());
file.write(data);
file.close();
//更新到数据库
Updater updater = new Updater();
updater.addField("chunkNo", chunkNo);
updater.addField("chunkTime", Sqls.nowTimestamp());
updater.addMust("fileMd5", chunk.getFileMd5());
updater.addMust("chunkSize", chunk.getChunkSize());
ORM.table().update(UpllChunk.class, updater);
}
/**
* 清理碎片数据
*
* @param fileMd5 文件MD5码
* @param chunkSize 碎片大小
* @throws Exception 可能的异常
*/
public static void clearChunkData(String fileMd5, int chunkSize) throws Exception
{
ORM.table().delete(UpllChunk.class, fileMd5, chunkSize);
}
/********************************************************************************************/
//三种保存文件,1、从碎片最后一个保存,2、秒传共享,3、秒传拷贝文件
/********************************************************************************************/
/**
* 保存文件,正常从最后一个碎片中读取
*
* @param chunk 文件分块
* @param uploadRootDir 文件绝对目录
* @param fileLength 文件长度
* @param contentType 内容类型
* @return 上传文件
* @throws Exception 可能的异常
*/
public static UpllFile saveFile(UpllChunk chunk, String uploadRootDir, long fileLength, String contentType) throws Exception
{
//1.最后一个碎片后重命名文件
String fileTempPath = chunk.getFileTempPath();
String fileDir = Files.getFileDir(fileTempPath);
String fileCanonicalPath = getFileAvailablePath(fileDir, chunk.getFileName());
Files.renameFile(fileTempPath, fileCanonicalPath);
//2.插入文件表
String fileId = Ids.uuid();
String fileName = Files.getFileName(fileCanonicalPath);
String fileExt = chunk.getFileExt();
String fileRelativePath = Strings.removeStartsWith(fileCanonicalPath, uploadRootDir);
UpllFile file = new UpllFile();
file.setFileId(fileId);
file.setFileName(fileName);
file.setFileLength(fileLength);
file.setFileRelativePath(fileRelativePath);
file.setFileExt(chunk.getFileExt());
file.setFileMd5(chunk.getFileMd5());
file.setFileUrl(_PATH_UPLOAD_LARGE_PREFIX_ + fileId + Files.fixFileExt(fileExt));
file.setFileTime(Sqls.nowTimestamp());
file.setFileType(contentType);
ORM.table().replace(file);
//3.清理碎片表
clearChunkData(chunk.getFileMd5(), chunk.getChunkSize());
//4.返回结果
return file;
}
/**
* 秒传,从另一个文件拷贝属性过来,共享同一个路径
*
* @param oFile 原始文件对象
* @return 上传文件
* @throws Exception 可能的异常
*/
public static UpllFile secondSaveFile(UpllFile oFile) throws Exception
{//秒传
String fileId = Ids.uuid();
String fileExt = oFile.getFileExt();
UpllFile file = new UpllFile();
file.setFileId(fileId);
file.setFileExt(fileExt);
file.setFileName(oFile.getFileName());
file.setFileLength(oFile.getFileLength());
file.setFileMd5(oFile.getFileMd5());
file.setFileRelativePath(oFile.getFileRelativePath());
file.setFileUrl(_PATH_UPLOAD_LARGE_PREFIX_ + fileId + Files.fixFileExt(fileExt));
file.setFileTime(Sqls.nowTimestamp());
file.setFileType(oFile.getFileType());
ORM.table().replace(file);
return file;
}
/**
* 秒传&拷贝,拷贝一个副本
*
* @param oFile 原始文件对象
* @return 上传文件
* @throws Exception 可能的异常
*/
public static UpllFile secondCopyFile(UpllFile oFile, String fileDir, String fileName) throws Exception
{//秒传拷贝文件
String fileId = Ids.uuid();
String fileCanonicalPath = getFileAvailablePath(fileDir, fileName);
String fileExt = oFile.getFileExt();
//拷贝文件
String oFileCanonicalPath = Zuls.getFilePath(oFile);
Files.copyFile(oFileCanonicalPath, fileCanonicalPath);
String uploadRootDir = Zuls.getUploadRootDir();
String fileRelativePath = Strings.removeStartsWith(fileCanonicalPath, uploadRootDir);
//保存到数据库
UpllFile file = new UpllFile();
file.setFileId(fileId);
file.setFileName(fileName);
file.setFileExt(fileExt);
file.setFileLength(oFile.getFileLength());
file.setFileMd5(oFile.getFileMd5());
file.setFileRelativePath(fileRelativePath);
file.setFileUrl(_PATH_UPLOAD_LARGE_PREFIX_ + fileId + Files.fixFileExt(fileExt));
file.setFileTime(Sqls.nowTimestamp());
file.setFileType(oFile.getFileType());
ORM.table().replace(file);
return file;
}
/********************************************************************************************/
//内部方法
/********************************************************************************************/
/** 获取一个碎片的文件名称,格式zhiqim_${fileMd5}.upll */
private static String getFileChunkPath(String fileDir, String fileMd5)
{
fileDir = Strings.addEndsWith(fileDir, "/");
return new StringBuilder(fileDir)
.append(_CHUNK_FILE_NAME_PREFIX_).append(fileMd5).append(_CHUNK_FILE_NAME_EXT_)
.toString();
}
/** 获取一个可用的文件名称,有重名的,后面加(n) */
private static String getFileAvailablePath(String fileDir, String fileName)
{
String fileExt = Files.getFileExt(fileName);
if (!Validates.isEmpty(fileExt))
{
fileExt = "." + fileExt;
fileName = Strings.trimRight(fileName, fileExt);
}
fileDir = Strings.addEndsWith(fileDir, "/");
String filePath = fileDir + fileName + fileExt;
int i = 1;
while (true)
{//判断相同文件名
File file = new File(filePath);
if (!file.exists())
return filePath;
filePath = fileDir + fileName + "("+ i +")" + fileExt;
i++;
}
}
}