1. 程式人生 > 其它 >使用JUnRar在Linux系統解壓檔案的"檔案丟失"問題

使用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 異常

至此,問題解決