在 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 上獲取。