在 Java 中提取 Tar 文件
一、簡介
在本教程中,我們將探索可用於提取tar
檔案的不同 Java 庫。 tar
格式最初是一種基於 Unix 的實用程序,用於將未壓縮的文件打包在一起。但如今,使用gzip
壓縮tar
檔案已非常普遍。因此,我們將了解壓縮與未壓縮的tar
存檔如何影響我們的代碼。
2. 創建實現的基類
為了避免樣板文件,讓我們從一個抽像類開始,我們將使用它作為實現的基礎。此類將定義一個抽象方法untar()
,它將執行提取:
public abstract class TarExtractor {
private InputStream tarStream;
private boolean gzip;
private Path destination;
// ...
public abstract void untar() throws IOException;
}
現在,讓我們為基類定義幾個構造函數。主構造函數將接收一個tar
存檔作為InputStream
,無論內容是否被壓縮,以及文件提取到的Path
:
<span style="color: #333333;font-family: Consolas, Monaco, monospace"><span style="background-color: #ffffff">protected</span></span> TarExtractor(InputStream in, boolean gzip, Path destination) throws IOException {
this.tarStream = in;
this.gzip = gzip;
this.destination = destination;
Files.createDirectories(destination);
}
最重要的是,我們為使用Files.createDirectories()
提取的文件創建基本目錄結構。這樣,我們就不需要自己創建目標文件夾。為了簡單起見,我們使用布爾值來定義我們的存檔是否使用gzip
。因此,我們不需要編寫代碼來通過其內容來檢測實際的文件類型。
然後,在第二個構造函數中,我們將接受tar
存檔的Path
,並根據文件名確定它是否被壓縮。請注意,這依賴於文件名是否正確:
<span style="color: #333333;font-family: Consolas, Monaco, monospace"><span style="background-color: #ffffff">protected</span></span> TarExtractor(Path tarFile, Path destination) throws IOException {
this(Files.newInputStream(tarFile), tarFile.endsWith("gz"), destination);
}
最後,為了簡化測試,我們將創建一個類,該類的方法從資源文件夾返回tar
存檔:
public interface Resources {
static InputStream tarGzFile() {
return Resources.class.getResourceAsStream("/untar/test.tar.gz");
}
}
這可以是使用gzip
壓縮的任何tar
存檔。我們只是將其放入一個方法中以避免“流關閉”錯誤。
3. 使用 Apache Commons 壓縮進行提取
在我們的第一個實現中,我們將使用 Apache Commons 庫[commons-compress](https://mvnrepository.com/artifact/org.apache.commons/commons-compress/1.23.0)
:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.23.0</version>
</dependency>
該解決方案涉及實例化TarArchiveInputStream,
它將接收我們的存檔流。然後,如果使用gzip
我們需要將其包裝在GzipCompressorInputStream
中:
public class TarExtractorCommonsCompress extends TarExtractor {
protected TarExtractorCommonsCompress(InputStream in, boolean gzip, Path destination) throws IOException {
super(in, gzip, destination);
}
public void untar() throws IOException {
try (BufferedInputStream inputStream = new BufferedInputStream(getTarStream());
TarArchiveInputStream tar = new TarArchiveInputStream(
isGzip() ? new GzipCompressorInputStream(inputStream) : inputStream)) {
ArchiveEntry entry;
while ((entry = tar.getNextEntry()) != null) {
Path extractTo = getDestination().resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(extractTo);
} else {
Files.copy(tar, extractTo);
}
}
}
}
}
首先,我們迭代TarArchiveInputStream
。為此,我們必須檢查getNextEntry()
是否返回ArchiveEntry
。然後,如果它是一個目錄,我們將相對於目標文件夾創建它。這樣,我們在其中寫入文件時就不會出現錯誤。否則,我們使用Files.copy()
從tar
到我們想要提取它的位置。
讓我們通過將存檔內容提取到任意文件夾來測試它:
@Test
public void givenTarGzFile_whenUntar_thenExtractedToDestination() throws IOException {
Path destination = Paths.get("/tmp/commons-compress-gz");
new TarExtractorCommonsCompress(Resources.tarGzFile(), true, destination).untar();
try (Stream files = Files.list(destination)) {
assertTrue(files.findFirst().isPresent());
}
}
如果我們的存檔沒有使用gzip
,我們只需要在實例化TarExtractorCommonsCompress
對象時傳遞false
即可。另請注意, GzipCompressorInputStream
可以提取gzip
以外的格式。
4. 使用 Apache Ant 提取
借助 Apache [ant](https://mvnrepository.com/artifact/org.apache.ant/ant/1.10.13)
,我們可以接近核心 Java 實現,因為我們可以使用java.util
中的GZIPInputStream
以防我們的存檔使用gzip
:
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.13</version>
</dependency>
我們將有一個非常相似的實現:
public class TarExtractorAnt extends TarExtractor {
// standard delegate constructor
public void untar() throws IOException {
try (TarInputStream tar = new TarInputStream(new BufferedInputStream(
isGzip() ? new GZIPInputStream(getTarStream()) : getTarStream()))) {
TarEntry entry;
while ((entry = tar.getNextEntry()) != null) {
Path extractTo = getDestination().resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(extractTo);
} else {
Files.copy(tar, extractTo);
}
}
}
}
}
這裡的邏輯是相同的,但我們使用 Apache Ant 中的TarInputStream
和TarEntry
而不是TarArchiveInputStream
和ArchiveEntry
。我們可以像之前的解決方案一樣進行測試:
@Test
public void givenTarGzFile_whenUntar_thenExtractedToDestination() throws IOException {
Path destination = Paths.get("/tmp/ant-gz");
new TarExtractorAnt(Resources.tarGzFile(), true, destination).untar();
try (Stream files = Files.list(destination)) {
assertTrue(files.findFirst().isPresent());
}
}
5. 使用 Apache VFS 提取
在最後一個示例中,我們將使用 Apache [commons-vfs2](https://mvnrepository.com/artifact/org.apache.commons/commons-vfs2/2.9.0)
,它通過單個 API支持不同的文件系統方案。其中之一是tar
檔案:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-vfs2</artifactId>
<version>2.9.0</version>
</dependency>
但是,由於我們正在從輸入流中讀取數據,因此我們首先需要將流保存到臨時文件中,以便隨後生成 URI:
public class TarExtractorVfs extends TarExtractor {
// standard delegate constructor
public void untar() throws IOException {
Path tmpTar = Files.createTempFile("temp", isGzip() ? ".tar.gz" : ".tar");
Files.copy(getTarStream(), tmpTar);
// ...
Files.delete(tmpTar);
}
}
我們將在提取結束時刪除臨時文件。接下來,我們將獲取FileSystemManager
的實例並將文件 URI 解析為FileObject
,然後使用它來迭代存檔內容:
FileSystemManager fsManager = VFS.getManager();
String uri = String.format("%s:file://%s", isGzip() ? "tgz" : "tar", tmpTar);
FileObject tar = fsManager.resolveFile(uri);
請注意,對於resolveFile()
,如果我們使用gzip
,我們會以不同的方式構建URI,並在其前面加上“tgz”
(表示tar
+ gzip
)而不是“tar”
前綴。然後,最後,我們迭代我們的存檔內容,提取每個文件:
for (FileObject entry : tar) {
Path extractTo = Paths.get(
getDestination().toString(), entry.getName().getPath());
if (entry.isReadable() && entry.getType() == FileType.FILE) {
Files.createDirectories(extractTo.getParent());
try (FileContent content = entry.getContent();
InputStream stream = content.getInputStream()) {
Files.copy(stream, extractTo);
}
}
}
而且,因為我們可能會亂序接收項目,所以我們將檢查我們的條目是否是一個文件,並在其父級上調用createDirectories()
。這樣,我們就不會在創建目錄之前冒險創建文件。最後,由於entry
路徑返回時帶有前導斜杠,因此我們不會像以前的實現那樣使用Paths.resolve()
來創建目標文件。我們來測試一下:
@Test
public void givenTarGzFile_whenUntar_thenExtractedToDestination() throws IOException {
Path destination = Paths.get("/tmp/vfs-gz");
new TarExtractorVfs(Resources.tarGzFile(), true, destination).untar();
try (Stream files = Files.list(destination)) {
assertTrue(files.findFirst().isPresent());
}
}
僅當我們已在項目中使用 VFS 時,此解決方案才有用,因為它需要更多代碼。
六,結論
在本文中,我們學習瞭如何使用不同的庫提取tar
檔案。我們的實現從基類擴展而來,減少了代碼並使它們更易於使用。
與往常一樣,源代碼可以在 GitHub 上獲取。