java-文件分割
在进行 minio 大文件上传时,需要用到文件分割,找了一圈没有合适的,于是造了一个轮子
原理
运用 RandomAccessFile 操作文件
文件分割
采用多线程方式
FileSplitRunnable
线程处理文件分割
@Slf4j
public class FileSplitRunnable implements Runnable {
private static final int READ_BYES = 2*1024;
/**
* 要分割的文件
*/
private File originFile;
/**
* 文件分割起始位置
*/
private long startPosition;
/**
* 每个分割文件大小
*/
private long totalSize;
/**
* 分割文件名称
*/
private String filePartName;
/**
* 文件分割同步器
*/
private FileTask task;
public FileSplitRunnable(File originFile, long startPosition, long totalSize, String filePartName, FileTask task) {
this.originFile = originFile;
this.startPosition = startPosition;
this.totalSize = totalSize;
this.filePartName = filePartName;
this.task = task;
}
@Override
public void run() {
log.info("{} split file {} start at position {}", Thread.currentThread().getName(), filePartName, startPosition);
RandomAccessFile accessFile = null;
FileOutputStream fos = null;
long remian = totalSize;
byte[] b = new byte[READ_BYES];
int read = 0;
try {
//采用追加模式
accessFile = new RandomAccessFile(originFile, "r");
accessFile.seek(startPosition);
fos = new FileOutputStream(filePartName, true);
while (remian > 0 && read!=-1) {
log.info("{} read size {} remain {}", Thread.currentThread().getName(), read, remian);
if (remian < READ_BYES) {
b = new byte[(int) remian];
read = accessFile.read(b);
if (read!=-1){
fos.write(b, 0, read);
remian = 0;
}
}
read = accessFile.read(b);
if (read!=-1){
fos.write(b, 0, read);
remian -= READ_BYES;
}
}
fos.flush();
//增加成功
task.taskDoneIncr();
} catch (IOException e) {
throw new RuntimeException("file split error" + filePartName);
} finally {
try {
if (fos != null) {
fos.close();
}
if (accessFile != null) {
accessFile.close();
}
} catch (IOException e) {
log.error("###### FileSplitRunnable file close error", e);
}
task.countDown();
}
}
}
FileTask 文件同步器
public class FileTask {
private CountDownLatch latch;
private int task;
private AtomicInteger cnt = new AtomicInteger(0);
public FileTask(int tasks) {
this.task = tasks;
this.latch = new CountDownLatch(tasks);
}
/**
* 带超时时间的等待任务超时
* @param timeOut 超时时间
* @param timeUnit
* @return
* @throws InterruptedException
*/
public boolean await(long timeOut, TimeUnit timeUnit) throws InterruptedException {
return latch.await(timeOut, timeUnit);
}
/**
* 等待任务直到完成
* @throws InterruptedException
*/
public void await() throws InterruptedException {
latch.await();
}
/**
* 查询任务是否结束
* @return
*/
public boolean isDone() {
try {
boolean await = await(0, TimeUnit.MILLISECONDS);
return await && (cnt.get() == task);
} catch (InterruptedException e) {
return false;
}
}
/**
* 任务减一
*/
public void countDown() {
latch.countDown();
}
/**
* 当任务完成没有异常时+1
* 即完成成功的任务
*/
public void taskDoneIncr() {
cnt.getAndIncrement();
}
}
文件合并
FileMergeRunnable
public class FileMergeRunnable implements Runnable {
private static final int READ_BYES = 2 * 1024;
/**
* 合并后的新文件名称
*/
private String finalFileName;
/**
* 每个文件开始合并的位置
*/
private long startPosition;
/**
* 分片文件
*/
private File partFile;
/**
* 文件同步器
*/
private FileTask task;
public FileMergeRunnable(String finalFileName, long startPosition, File partFile, FileTask task) {
this.finalFileName = finalFileName;
this.startPosition = startPosition;
this.partFile = partFile;
this.task = task;
}
@Override
public void run() {
log.info("{} merge file {} at position {}", Thread.currentThread().getName(), partFile.getName(), startPosition);
RandomAccessFile accessFile = null;
FileInputStream fis = null;
try {
accessFile = new RandomAccessFile(finalFileName, "rw");
accessFile.seek(startPosition);
byte[] b = new byte[READ_BYES];
int len = 0;
fis = new FileInputStream(partFile);
while ((len = fis.read(b)) != -1) {
accessFile.write(b, 0, len);
}
task.taskDoneIncr();
} catch (IOException e) {
log.error("######### FileMergeRunnable merge error", e);
} finally {
try {
if (fis != null) {
fis.close();
}
if (accessFile != null) {
accessFile.close();
}
} catch (IOException e) {
log.error("######### FileMergeRunnable merge close error", e);
}
task.countDown();
}
}
}
FileSpliter
文件分割,合并工具类
@Slf4j
public class FileSpliter {
/**
* 文件分割
*
* @param fileName 原始文件
* @param perPartBytes 每个文件大小: byte
* @param executor
* @return
*/
public static List<String> split(String fileName, int perPartBytes, ThreadPoolExecutor executor) {
Assert.notNull(executor, "executor must not null!!");
File inputFile = new File(fileName);
List<String> list = new ArrayList<>();
String filePartNameSuffix = fileName.concat(".part");
//向上取整, 如果不使用double类型的话,计算出来是整数,
int count = (int) Math.ceil(inputFile.length() / (double)perPartBytes);
FileTask task = new FileTask(count);
for (int i = 0; i <= count; i++) {
String filePartName = filePartNameSuffix.concat(i + "");
FileSplitRunnable runnable = new FileSplitRunnable(inputFile, i * perPartBytes, perPartBytes, filePartName, task);
if (executor != null) {
executor.execute(runnable);
list.add(filePartName);
}
}
try {
task.await();
} catch (InterruptedException e) {
throw new RuntimeException("File Split error", e);
}
log.info("file split is done: " + task.isDone());
return list;
}
/**
* 文件分片合并
*
* @param finalFileName 最终合并文件名称
* @param path: 分片文件所在目录
* @param partFileNamePrefix 分片文件前缀
* @param executor
* @return
*/
public static String merge(String finalFileName, String path, String partFileNamePrefix, ThreadPoolExecutor executor) {
Assert.notNull(executor, "executor must not null!!");
File tmp = new File(finalFileName);
try {
tmp.createNewFile();
} catch (IOException e) {
log.error("create merge file error", e);
throw new RuntimeException(e);
}
//搜索该路径下partFileNamePrefix前缀文件,即分片文件
List<File> partFiles = getPartFiles(path, partFileNamePrefix);
merge(finalFileName, partFiles, executor);
return finalFileName;
}
/**
* 分片文件合并
*
* @param finalFileName 合并后的新文件
* @param partFiles 分片文件
* @param executor
* @return
*/
public static String merge(String finalFileName, List<File> partFiles, ThreadPoolExecutor executor) {
if (CollectionUtil.isEmpty(partFiles)) {
throw new RuntimeException("合并文件不能为空");
}
Assert.notNull(executor, "executor must not null!!");
File tmp = new File(finalFileName);
try {
if (!tmp.exists()) {
tmp.createNewFile();
}
} catch (IOException e) {
log.error("create merge file error", e);
throw new RuntimeException(e);
}
//排序,按照分割的顺序
Collections.sort(partFiles, (o1, o2) -> {
if (o1.getName().compareTo(o1.getName()) < 0) {
return -1;
}
return 1;
});
partFiles.stream().forEach(a -> System.out.println(a.getName()));
int count = partFiles.size();
FileTask task = new FileTask(count);
long perPartFileSize = partFiles.get(0).length();
for (int i = 0; i < count; i++) {
FileMergeRunnable mergeRunnable = new FileMergeRunnable(finalFileName, i * perPartFileSize, partFiles.get(i), task);
executor.execute(mergeRunnable);
}
try {
task.await();
} catch (InterruptedException e) {
throw new RuntimeException("File merge error", e);
}
log.info("file merge is done: " + task.isDone());
//合并完成后删除
partFiles.stream().forEach(f -> f.delete());
return finalFileName;
}
/**
* 查找该路径下前缀是 partFileNamePrefix 的文件
*
* @param path
* @param partFileNamePrefix
* @return
*/
private static List<File> getPartFiles(String path, String partFileNamePrefix) {
File dir = new File(path);
File[] files = dir.listFiles((dir1, name) -> {
String prefix = partFileNamePrefix.toLowerCase();
return name.toLowerCase().startsWith(prefix);
});
return Arrays.stream(files).filter(f -> f.isFile()).collect(Collectors.toList());
}
}
测试
public class FileTest {
//文件分割
@Test
public void splitFileTest() {
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
String fileName = "F:\\test\\Java核心技术卷.pdf";
List<String> split = FileSpliter.split(fileName, 30 * 1024 * 1024, executor);
System.out.println(split);
executor.shutdown();
}
@Test
public void mergeFileTest() {
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
String finalFileName = "F:\\test\\Java核心技术卷-NEW.pdf";
String path = "F:\\test";
String prefix = "Java核心技术卷.pdf.part";
FileSpliter.merge(finalFileName, path, prefix, executor);
executor.shutdown();
}
//视频分割
@Test
public void splitVideoTest() {
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
String fileName = "F:\\test\\测试.mp4";
List<String> split = FileSpliter.split(fileName, 30 * 1024 * 1024, executor);
System.out.println(split);
executor.shutdown();
}
@Test
public void mergeVideoTest() {
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
String finalFileName = "F:\\test\\测试-NEW.mp4";
String path = "F:\\test";
String prefix = "测试.mp4.part";
FileSpliter.merge(finalFileName, path, prefix, executor);
executor.shutdown();
}
}
分割测试:
合并测试:
也可以正常打开
good luck!