增强 ArchiveFileTree (#4547)

This commit is contained in:
Glavo
2025-09-29 15:19:37 +08:00
committed by GitHub
parent 8a89baee09
commit e90d5185f6
3 changed files with 176 additions and 17 deletions

View File

@@ -24,15 +24,20 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.FileTime;
import java.util.*;
/**
* @author Glavo
*/
public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Closeable {
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public abstract class ArchiveFileTree<R, E extends ArchiveEntry> implements Closeable {
public static ArchiveFileTree<?, ?> open(Path file) throws IOException {
Path namePath = file.getFileName();
@@ -50,23 +55,55 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
}
}
protected final F file;
protected final Dir<E> root = new Dir<>("");
protected final R reader;
protected final Dir<E> root = new Dir<>("", "");
public ArchiveFileTree(F file) {
this.file = file;
public ArchiveFileTree(R reader) {
this.reader = reader;
}
public F getFile() {
return file;
public R getReader() {
return reader;
}
public Dir<E> getRoot() {
return root;
}
public @Nullable E getEntry(@NotNull String entryPath) {
Dir<E> dir = root;
String fileName;
if (entryPath.indexOf('/') < 0) {
fileName = entryPath;
if (fileName.isEmpty())
return root.getEntry();
} else {
String[] path = entryPath.split("/");
if (path.length == 0)
return root.getEntry();
for (int i = 0; i < path.length - 1; i++) {
String item = path[i];
if (item.isEmpty())
continue;
dir = dir.getSubDirs().get(item);
if (dir == null)
return null;
}
fileName = path[path.length - 1];
E entry = dir.getFiles().get(fileName);
if (entry != null)
return entry;
}
Dir<E> subDir = dir.getSubDirs().get(fileName);
return subDir != null ? subDir.getEntry() : null;
}
protected void addEntry(E entry) throws IOException {
String[] path = entry.getName().split("/");
List<String> pathList = Arrays.asList(path);
Dir<E> dir = root;
@@ -81,7 +118,9 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
throw new IOException("A file and a directory have the same name: " + entry.getName());
}
dir = dir.subDirs.computeIfAbsent(item, Dir::new);
final int nameEnd = i + 1;
dir = dir.subDirs.computeIfAbsent(item, name ->
new Dir<>(name, String.join("/", pathList.subList(0, nameEnd))));
}
if (entry.isDirectory()) {
@@ -106,6 +145,55 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
public abstract InputStream getInputStream(E entry) throws IOException;
public @NotNull InputStream getInputStream(String entryPath) throws IOException {
E entry = getEntry(entryPath);
if (entry == null)
throw new FileNotFoundException("Entry not found: " + entryPath);
return getInputStream(entry);
}
public byte[] readBinaryEntry(@NotNull E entry) throws IOException {
try (InputStream input = getInputStream(entry)) {
return input.readAllBytes();
}
}
public String readTextEntry(@NotNull String entryPath) throws IOException {
E entry = getEntry(entryPath);
if (entry == null)
throw new FileNotFoundException("Entry not found: " + entryPath);
return readTextEntry(entry);
}
public String readTextEntry(@NotNull E entry) throws IOException {
return new String(readBinaryEntry(entry), StandardCharsets.UTF_8);
}
protected void copyAttributes(@NotNull E source, @NotNull Path targetFile) throws IOException {
FileTime lastModifiedTime = source.getLastModifiedTime();
if (lastModifiedTime != null)
Files.setLastModifiedTime(targetFile, lastModifiedTime);
}
public void extractTo(@NotNull String entryPath, @NotNull Path targetFile) throws IOException {
E entry = getEntry(entryPath);
if (entry == null)
throw new FileNotFoundException("Entry not found: " + entryPath);
extractTo(entry, targetFile);
}
public void extractTo(@NotNull E entry, @NotNull Path targetFile) throws IOException {
try (InputStream input = getInputStream(entry)) {
Files.copy(input, targetFile, StandardCopyOption.REPLACE_EXISTING);
}
try {
copyAttributes(entry, targetFile);
} catch (Throwable e) {
LOG.warning("Failed to copy attributes to " + targetFile, e);
}
}
public abstract boolean isLink(E entry);
public abstract String getLink(E entry) throws IOException;
@@ -117,13 +205,15 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
public static final class Dir<E extends ArchiveEntry> {
private final String name;
private final String fullName;
private E entry;
final Map<String, Dir<E>> subDirs = new HashMap<>();
final Map<String, E> files = new HashMap<>();
public Dir(String name) {
public Dir(String name, String fullName) {
this.name = name;
this.fullName = fullName;
}
public boolean isRoot() {
@@ -134,6 +224,11 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
return name;
}
/// Get the normalized full path. Leading `/` and all `.` in the path will be removed.
public @NotNull String getFullName() {
return fullName;
}
public @Nullable E getEntry() {
return entry;
}

View File

@@ -20,11 +20,13 @@ package org.jackhuang.hmcl.util.tree;
import kala.compress.archivers.tar.TarArchiveEntry;
import kala.compress.archivers.tar.TarArchiveReader;
import org.jetbrains.annotations.NotNull;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.util.zip.GZIPInputStream;
/**
@@ -98,9 +100,22 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
this.shutdownHook = null;
}
@Override
protected void copyAttributes(@NotNull TarArchiveEntry source, @NotNull Path targetFile) throws IOException {
var fileAttributeView = Files.getFileAttributeView(targetFile, BasicFileAttributeView.class);
if (fileAttributeView == null)
return;
fileAttributeView.setTimes(
source.getLastModifiedTime(),
source.getLastAccessTime(),
source.getCreationTime()
);
}
@Override
public InputStream getInputStream(TarArchiveEntry entry) throws IOException {
return file.getInputStream(entry);
return reader.getInputStream(entry);
}
@Override
@@ -121,7 +136,7 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
@Override
public void close() throws IOException {
try {
file.close();
reader.close();
} finally {
if (tempFile != null) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);

View File

@@ -20,9 +20,17 @@ package org.jackhuang.hmcl.util.tree;
import kala.compress.archivers.zip.ZipArchiveEntry;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.util.EnumSet;
import java.util.Set;
/**
* @author Glavo
@@ -51,12 +59,53 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
@Override
public void close() throws IOException {
if (closeReader)
file.close();
reader.close();
}
@Override
@SuppressWarnings("OctalInteger")
protected void copyAttributes(@NotNull ZipArchiveEntry source, @NotNull Path targetFile) throws IOException {
BasicFileAttributeView targetView = Files.getFileAttributeView(targetFile, PosixFileAttributeView.class);
// target might not support posix even if source does
if (targetView == null)
targetView = Files.getFileAttributeView(targetFile, BasicFileAttributeView.class);
if (targetView == null)
return;
targetView.setTimes(
source.getLastModifiedTime(),
source.getLastAccessTime(),
source.getCreationTime()
);
int unixMode = source.getUnixMode();
if (unixMode != 0 && targetView instanceof PosixFileAttributeView posixView) {
Set<PosixFilePermission> permissions = EnumSet.noneOf(PosixFilePermission.class);
// Owner permissions
if ((unixMode & 0400) != 0) permissions.add(PosixFilePermission.OWNER_READ);
if ((unixMode & 0200) != 0) permissions.add(PosixFilePermission.OWNER_WRITE);
if ((unixMode & 0100) != 0) permissions.add(PosixFilePermission.OWNER_EXECUTE);
// Group permissions
if ((unixMode & 0040) != 0) permissions.add(PosixFilePermission.GROUP_READ);
if ((unixMode & 0020) != 0) permissions.add(PosixFilePermission.GROUP_WRITE);
if ((unixMode & 0010) != 0) permissions.add(PosixFilePermission.GROUP_EXECUTE);
// Others permissions
if ((unixMode & 0004) != 0) permissions.add(PosixFilePermission.OTHERS_READ);
if ((unixMode & 0002) != 0) permissions.add(PosixFilePermission.OTHERS_WRITE);
if ((unixMode & 0001) != 0) permissions.add(PosixFilePermission.OTHERS_EXECUTE);
posixView.setPermissions(permissions);
}
}
@Override
public InputStream getInputStream(ZipArchiveEntry entry) throws IOException {
return getFile().getInputStream(entry);
return getReader().getInputStream(entry);
}
@Override
@@ -66,7 +115,7 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
@Override
public String getLink(ZipArchiveEntry entry) throws IOException {
return getFile().getUnixSymlink(entry);
return getReader().getUnixSymlink(entry);
}
@Override