使用JUnRar在Linux系统解压文件的"文件丢失"问题
标题说明
此问题实际上并不是讨论JUnRar本身在Linux系统中执行解压而导致的文件缺失,而仅仅是由于业务代码问题而出现了类似于"文件丢失"的现象,其本质实际上是解压出的文件结构与预期不符而导致的下游代码执行问题,但因标题字数限制而采用了稍有出入的表达; 因此将"文件丢失"使用引号标注;
表层现象
CRM系统存在一个"报告订阅"功能,主要使用人员是基金经理,首先需要用户定义报告模板,基金经理定期将基金的月、季、年等周期的报告提交到系统,系统负责归档报告并在指定时间发送给设定的客户;基金经理提交的报告为压缩包形式,在特定时间开启ftp端口接受文件;系统有一个定时任务每隔一定时间进行文件扫描,将扫描到的文件进行归档;另一个定时任务负责将已归档文件解压并统一发送;
最近在生产环境中发现某个报告已经归档,但迟迟没有发送出去,且文件状态始终停留在发送中;
问题定位
因为是外包公司,生产数据的问题排查受限,最后只得到了技术经理给我截取的一段报错日志与报错文件,日志内容如下
crm-schedule-0] o.apache.http.impl.execchain.RetryExec : I/O exception (java.io.FileNotFoundException) caught when processing request to {}->http://10.XX.XXX.XX:8500: /opt/report/archive20210524XXXXXX/XXX190创XXXX95号组合周报(2021年05月17日-2021年05月21日)\账户信息(范本)X利95号20210521.xlsx (Invalid argument)
2021-05-24 17:25:05.441 INFO 30817 --- [pyamc-crm-schedule-0] o.apache.http.impl.execchain.RetryExec : Retrying request to {}->http://10.XX.XXX.XX:8500
2021-05-24 17:25:05.442 INFO 30817 --- [pyamc-crm-schedule-0] o.apache.http.impl.execchain.RetryExec : I/O exception (java.io.FileNotFoundException) caught when processing request to {}->http://10.XX.XXX.XX:8500: /opt/report/archive20210524XXXXXX/XXX190创XXXX95号组合周报(2021年05月17日-2021年05月21日)\账户信息(范本)X利95号20210521.xlsx (Invalid argument)
2021-05-24 17:25:05.442 INFO 30817 --- [pyamc-crm-schedule-0] o.apache.http.impl.execchain.RetryExec : Retrying request to {}->http://10.XX.XXX.XX:8500
2021-05-24 17:25:05.444 INFO 30817 --- [pyamc-crm-schedule-0] o.apache.http.impl.execchain.RetryExec : I/O exception (java.io.FileNotFoundException) caught when processing request to {}->http://10.XX.XXX.XX:8500: /opt/report/archive20210524XXXXXX/XXX190创XXXX95号组合周报(2021年05月17日-2021年05月21日)\账户信息(范本)X利95号20210521.xlsx (Invalid argument)
2021-05-24 17:25:05.444 INFO 30817 --- [pyamc-crm-schedule-0] o.apache.http.impl.execchain.RetryExec : Retrying request to {}->http://10.XX.XXX.XX:8500
2021-05-24 17:25:05.446 ERROR 30817 --- [pyamc-crm-schedule-0] com.pyamc.core.utils.HttpClientUtil : 请求失败
java.io.FileNotFoundException: /opt/report/archive20210524XXXXXX/XXX190创XXXX95号组合周报(2021年05月17日-2021年05月21日)\账户信息(范本)X利95号20210521.xlsx (Invalid argument)
at java.io.FileInputStream.open0(Native Method) ~[na:1.8.0_211]
at java.io.FileInputStream.open(FileInputStream.java:195) ~[na:1.8.0_211]
at java.io.FileInputStream.<init>(FileInputStream.java:138) ~[na:1.8.0_211]
at org.apache.http.entity.mime.content.FileBody.writeTo(FileBody.java:115)
从报错中可以看出文件一级与上级的分隔符为反斜杠符号,实际上技术经理应该没有截取全,因为显然报错信息甚至都没有定位到具体对象,只能去翻了下代码,找到了调用点的函数签名
public static synchronized List<String> unrar(String srcRarPath, String dstDirectoryPath)
在本地使用问题文件测试了一下可以正常执行,基本可以确定问题在unrar函数的实现内部与路径获取有关的部分
翻了下unrar函数的具体实现,大致流程如下:
a = new Archive(new File(srcRarPath));
if (a != null) {
FileHeader fh = a.nextFileHeader();
while (fh != null) {
if (fh.isDirectory()) {
if (existZH(fh.getFileNameW())) {
fol = new File(dstDirectoryPath + File.separator
+ fh.getFileNameW());
} else {
fol = new File(dstDirectoryPath + File.separator
+ fh.getFileNameString());
}
fol.mkdirs();
} else { // 文件
if (existZH(fh.getFileNameW())) {
out = new File(dstDirectoryPath + File.separator
+ fh.getFileNameW().trim());
} else {
out = new File(dstDirectoryPath + File.separator
+ fh.getFileNameString().trim());
}
try {
filePathList.add(out.getPath());
if (!out.exists()) {
if (!out.getParentFile().exists()) {
out.getParentFile().mkdirs();
}
out.createNewFile();
}
FileOutputStream os = new FileOutputStream(out);
a.extractFile(fh, os);
os.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
fh = a.nextFileHeader();
}
a.close();
}
可以看出与路径相关且负责解压文件(相对于目录)的代码段为:
if (existZH(fh.getFileNameW())) {
out = new File(dstDirectoryPath + File.separator
+ fh.getFileNameW().trim());
} else {
out = new File(dstDirectoryPath + File.separator
+ fh.getFileNameString().trim());
}
try {
filePathList.add(out.getPath());
if (!out.exists()) {
if (!out.getParentFile().exists()) {
out.getParentFile().mkdirs();
}
out.createNewFile();
}
FileOutputStream os = new FileOutputStream(out);
a.extractFile(fh, os);
os.close();
} catch (Exception ex) {
ex.printStackTrace();
}
因为在本地测试无问题,推测是Linux路径分隔符相关的问题,把项目部在Docker上测试了下,发现在调用FileHeader对象的getNameW时获取的文件名为:
XXX190创XXXX95号组合周报(2021年05月17日-2021年05月21日)\账户信息(范本)X利95号20210521.xlsx
显然得到的是以Windows系统分隔符分隔的路径,问题定位完成
补充:
查看了JunRar的源码,发现其并未对Header中的路径分隔符做变更,而是直接以byte读取特定位的文件头,因此可以确定此分隔符是文件本身的属性;
推测两种可能:1、WinRAR压缩时的使用的分隔符就是Windows系统的分隔符,因为WinRAR是闭源产品; 2、分隔符使用的是文件压缩的宿主系统对应的路径分隔符;
使用WSL2安装rarlinux并进行文件压缩(version:5.3.2),压缩后进行解压缩,发现文件分割路径依旧是反斜杠(\),基本可以确定是可能1,但因为并没有使用原生Linux系统实验,结果可能存在一定偏差;
因为不论是哪种原因,为了能够正常处理文件,都需要在业务逻辑中手动处理分隔符,再继续探究意义不大,再加上时间问题没有进行更深入的研究;
问题解决
在拼接文件解压路径的逻辑中加入手动的分隔符替换,下为其中一部分的修改后代码:
if (existZH(fh.getFileNameW())) {
fol = new File(dstDirectoryPath + File.separator + fh.getFileNameW().replaceAll("\\\\", Matcher.quoteReplacement(File.separator)));
}
其中 Matcher.quoteReplacement(File.separator) 部分之所以不直接使用 File.separator 是因为使用了replaceAll函数,若系统分隔符为反斜杠会被识别为转义符号,抛出 java.lang.IllegalArgumentException: character to be escaped is missing 异常
至此,问题解决

浙公网安备 33010602011771号