增强 ArchiveFileTree (#4547)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user