Java Swing分割、合并文件软件(续)

SwingRandomAccessFile
placeholder image
admin 发布于:2022-07-29 22:34:05
阅读:loading

接前面一篇的《Java Swing分割、合并文件软件》实现,本篇文章主要是对其进行改良,前文中实现的.obj文件实际上是使用JDK的序列化保存的分割参数数据的对象文件,只需要读取后并反序列化拿到文件对象即可实现文件的还原,当文件是顺序写入时可不需要此数据文件,当文件是随机写入时,就必须依赖此文件才可还原出原始文件,所以当有了此文件后原始文件也变得不再安全,所以本篇文章主要是取消了对改文件的依赖,使用Apache Commons Crypto的对称加密将文件数据对象写入分段写入的最后一个文件片段中的开始部分,以“{base64编码}”的格式存储,同时再对设置的密码进行手尾加盐处理,使得加密的密文更加可靠,详细介绍如下文所示。

文件加密与分割

最后一个分段文件的开始部分增加了一段特殊的加密数据文本,存储原始文件的信息,参考如下图所示:

image.png

(最后一个片段文件的开始部分的密文)

image.png

(分割后的数据文件)

分割文件代码参考

package cn.chendd.core;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JOptionPane;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.util.IOUtils;

import cn.chendd.bo.FilePropBo;
import cn.chendd.utils.Constants;
import cn.chendd.utils.CryptoUtils;

/**
 * 按指定文件大小分割文件,并记录文件分割信息
 *
 * @author chendd
 */
public class SplitFile {

    /**
     * 分割单个文件大小,参与运算
     */
    private long blockLength;
    /**
     * 当前分割次数的位置
     */
    private long currentTotal;
    /**
     * 记录本次分割文件的信息
     */
    private FilePropBo propMap;
    /**
     * 排序,true为顺序写入,false为随机写入
     */
    private boolean order;

    public SplitFile(long blockLength, boolean order) {
        this.blockLength = blockLength;
        propMap = new FilePropBo();
        this.order = order;
    }

    public boolean split(File srcFile, String splitFolder) throws Exception {
        RandomAccessFile raf = null;
        try {
            //文件大小
            long fileLens = srcFile.length();
            if (fileLens <= this.blockLength) {
                JOptionPane.showMessageDialog(null, "设置要分割的文件大小 " + (this.blockLength / 1024 / 1024) + "M 大于文件的大小!",
                        "警告", JOptionPane.ERROR_MESSAGE);
                return false;
            }
            raf = new RandomAccessFile(srcFile, "r");
            //根据分割块大小,计算一共有多少块文件
            int blockCount = (int) (fileLens % this.blockLength);
            if (blockCount == 0) {
                blockCount = (int) (fileLens / this.blockLength);
            } else {
                blockCount = (int) (fileLens / this.blockLength) + 1;
            }
            //按快进行读取文件
            String srcFileName = srcFile.getName();
            //记录源文件名称,原始文件名称
            propMap.setFileName(srcFileName);
            propMap.setFileLength(this.getFileLength(srcFile));
            propMap.setFileSuffix(this.getFileSuffix(srcFileName));
            List<String> partList = new ArrayList<String>();
            int max = 0;
            for (int i = 0; i < blockCount; i++) {
                int label;
                if (order) {
                    label = i + 1;
                } else {
                    //不考虑会出现重复的
                    label = (10000 + (int) (Math.random() * 10000));
                }
                if (label > max) {
                    max = label;
                }
                File destFile = new File(splitFolder + File.separator + srcFileName + "." + label + ".part");
                if (destFile.exists()) {
                   destFile.delete();
                }
                String partFileName = destFile.getName();
                partList.add(partFileName);
                //如果当前是最后一个文件,则优先写入一段密文文本,记录还原文件相关的数据,用序列化文件
                if (i + 1 == blockCount) {
                    //碎片文件列表
                    propMap.setPartList(partList);
                   this.writePropFile(propMap, srcFile, destFile);
                }
                //分割文件
                splitFileDetail(destFile, raf);
            }
            return true;
        } finally {
            IOUtils.close(raf);
        }

    }

    /**
     * 将记录的文件相关的数据写入文件
     *
     * @param propMap  写入文件属性对象
     * @param lastFile 源文件名称
     */
    private void writePropFile(FilePropBo propMap, File srcFile, File lastFile)
            throws Exception {
       BufferedWriter out = null;
        String json = JSONObject.toJSONString(propMap);
        try {
           out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(lastFile)));
            CryptoUtils crypto = CryptoUtils.newInstance();
            String encrypt = crypto.encrypt(json.getBytes());
            out.write(Constants.CRYPTO_PREFIX + encrypt + Constants.CRYPTO_SUFFIX);
        } finally {
            IOUtils.close(out);
        }
    }

    private void splitFileDetail(File destFile, RandomAccessFile raf) throws Exception {
        byte b[] = new byte[1024 * 4];
        int lens = 0;
        //如果文件目录不存在,则创建
        if (!destFile.getParentFile().exists()) {
            destFile.getParentFile().mkdirs();
        }
        BufferedOutputStream bos = null;
        try {
            //设置开始读取的位置
            raf.seek(currentTotal);
            long currentMax = raf.getFilePointer() + this.blockLength;
            bos = new BufferedOutputStream(new FileOutputStream(destFile , true));
            while ((lens = raf.read(b)) != -1) {
                //判断文件读取的大小,避免已经读取超过每块的大小了
                if (currentTotal + lens > currentMax) {
                    break;
                }
                bos.write(b, 0, lens);
                currentTotal += lens;
            }
        } catch (Exception e) {
            throw new Exception(e);
        } finally {
            if (bos != null) {
                try {
                    bos.flush();
                    bos.close();
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
        }
    }

    /**
     * 根据文件名称获取文件后缀
     *
     * @param fileName 文件名称
     * @return 返回.zip或.exe等
     */
    private String getFileSuffix(String fileName) {
        int index = fileName.lastIndexOf(".");
        if (index == -1) {
            return "";
        }
        return fileName.substring(index);
    }

    /**
     * 获取文件大小
     *
     * @param srcFile 源文件
     * @return 文件大小long类型
     */
    private long getFileLength(File srcFile) {
        return srcFile.length();
    }

}

合并文件代码参考

package cn.chendd.core;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.util.IOUtils;

import cn.chendd.bo.FilePropBo;
import cn.chendd.utils.Constants;
import cn.chendd.utils.CryptoUtils;

/**
 * 合并文件
 *
 * @author chendd
 */
public class MergeFile {

    /**
     * 合并文件
     *
     * @param propFile 分割文件的属性文件路径
     */
    public static void mergeFile(String propFile) throws IOException, ClassNotFoundException {
        File file = new File(propFile);
        StringBuilder encryptBuilder = new StringBuilder();
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            //判断文件的首字母是否为“{”
            int begin = is.read();
            boolean beginMatch = Constants.CRYPTO_PREFIX.equals(Character.toString((char) begin));
            if (! beginMatch) {
                throw new RuntimeException("当前文件无法被识别,请确认!");
            }
            boolean endMatch = false;
            int lens;
            while ((lens = is.read()) != -1) {
                endMatch = Constants.CRYPTO_SUFFIX.equals(Character.toString((char) lens));
                if (endMatch) {
                    break;
                }
                encryptBuilder.append((char) lens);
            }
            //判断读取文件内容时是否有读取到“}”
            if (! endMatch) {
                throw new RuntimeException("当前文件无法被识别,请确认!");
            }
        } catch (Exception e) {
            throw new RuntimeException("验证文件出现错误:" + e.getMessage());
        } finally {
            IOUtils.close(is);
        }
        CryptoUtils encrypto = CryptoUtils.newInstance();
        FilePropBo fileProp;
        String encryptText = encryptBuilder.toString();
        try {
            String decrypto = encrypto.decrypto(encryptText);
            fileProp = JSONObject.parseObject(decrypto , FilePropBo.class);
        } catch (Exception e) {
            throw new RuntimeException("解密失败:" + e.getMessage());
        }
        restoreFile(file , fileProp);
    }

    /**
     * 根据文件分割信息还原文件
     * @param file 当前选中文件
     * @param fileProp 文件还原参数
     */
    private static void restoreFile(File file , FilePropBo fileProp) {
        //源文件名称
        String fileName = fileProp.getFileName();
        //碎片文件
        List<String> partList = fileProp.getPartList();
        File srcFile = new File(file.getParent() + File.separator + fileName);
        if (srcFile.exists()) {
            srcFile.delete();
        }
        BufferedOutputStream bos = null;
        try {
            bos = new BufferedOutputStream(new FileOutputStream(srcFile, true));
            for (String partName : partList) {
                String filePath = file.getParent() + File.separator + partName;
                //合并写单个文件
                restoreFilePart(file, bos, partName, filePath);
                bos.flush();
            }

        } catch (Exception e) {
            throw new RuntimeException("合并文件出现错误:" + e.getMessage());
        } finally {
            IOUtils.close(bos);
        }
    }

    /**
     * 重写某个分段文件内容
     * @param file 文件
     * @param bos IO流
     * @param partName 分段文件名称
     * @param filePath 文件路径
     */
    private static void restoreFilePart(File file, BufferedOutputStream bos, String partName, String filePath) {
        byte b[] = new byte[1024];
        InputStream is = null;
        try {
            is = new FileInputStream(filePath);
            int lens;
            ///最后一个文件理论上应该跳过开头部分
            if (file.getName().equals(partName)) {
                //最后一个文件,跳过加密部分的文本内容
                while ((lens = is.read()) != -1) {
                    boolean endMatch = Constants.CRYPTO_SUFFIX.equals(Character.toString((char) lens));
                    if (endMatch) {
                        break;
                    }
                }
            }
            while ((lens = is.read(b)) != -1) {
                bos.write(b, 0, lens);
            }
        } catch (Exception e) {
            throw new RuntimeException("合并子文件出现错误:" + e.getMessage());
        } finally {
            IOUtils.close(is);
        }
    }

}

文件MD5对比

/**
 * 
 */
package cn.chendd;

import java.io.File;
import java.io.FileInputStream;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;

/**
 * @author chendd
 *
 */
public class MainMd5 {

   /**
    * @param args
    */
   public static void main(String[] args) throws Exception {
      File srcFile = new File(MainMerge.FILE_FOLDER , "原始文件【luyten-0.5.4.exe】.exe");
      byte[] srcByte = DigestUtils.md5(new FileInputStream(srcFile));
      System.out.println("原始文件MD5:" + new String(Base64.encodeBase64(srcByte)));
      
      File targetFile = new File(MainMerge.FILE_FOLDER , "luyten-0.5.4.exe");
      byte[] targetByte = DigestUtils.md5(new FileInputStream(targetFile));
      System.out.println("还原文件MD5:" + new String(Base64.encodeBase64(targetByte)));
      
      
   }

}

源码下载

原版分割、合并文件的实现可见https://gitee.com/88911006/chendd-examples/tree/master/split_merge下载,本次只是简单的优化实现(实践文件开始位置文本的插入和加密以及对文件MD5的验证),所以并未修改原版的代码,更没有增强使其变身为exe的可执行文件,所以对本例实现的分割和合并文件感兴趣的可以下载此处的附件下载split_file_example.zip

 点赞


 发表评论

当前回复:作者

 评论列表


留言区