java-文件分割

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!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值