增强 ArchiveFileTree (#4547)
This commit is contained in:
@@ -24,15 +24,20 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import org.jetbrains.annotations.UnmodifiableView;
|
import org.jetbrains.annotations.UnmodifiableView;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
* @author Glavo
|
|
||||||
*/
|
/// @author Glavo
|
||||||
public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Closeable {
|
public abstract class ArchiveFileTree<R, E extends ArchiveEntry> implements Closeable {
|
||||||
|
|
||||||
public static ArchiveFileTree<?, ?> open(Path file) throws IOException {
|
public static ArchiveFileTree<?, ?> open(Path file) throws IOException {
|
||||||
Path namePath = file.getFileName();
|
Path namePath = file.getFileName();
|
||||||
@@ -50,23 +55,55 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final F file;
|
protected final R reader;
|
||||||
protected final Dir<E> root = new Dir<>("");
|
protected final Dir<E> root = new Dir<>("", "");
|
||||||
|
|
||||||
public ArchiveFileTree(F file) {
|
public ArchiveFileTree(R reader) {
|
||||||
this.file = file;
|
this.reader = reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public F getFile() {
|
public R getReader() {
|
||||||
return file;
|
return reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dir<E> getRoot() {
|
public Dir<E> getRoot() {
|
||||||
return root;
|
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 {
|
protected void addEntry(E entry) throws IOException {
|
||||||
String[] path = entry.getName().split("/");
|
String[] path = entry.getName().split("/");
|
||||||
|
List<String> pathList = Arrays.asList(path);
|
||||||
|
|
||||||
Dir<E> dir = root;
|
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());
|
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()) {
|
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 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 boolean isLink(E entry);
|
||||||
|
|
||||||
public abstract String getLink(E entry) throws IOException;
|
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> {
|
public static final class Dir<E extends ArchiveEntry> {
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private final String fullName;
|
||||||
private E entry;
|
private E entry;
|
||||||
|
|
||||||
final Map<String, Dir<E>> subDirs = new HashMap<>();
|
final Map<String, Dir<E>> subDirs = new HashMap<>();
|
||||||
final Map<String, E> files = new HashMap<>();
|
final Map<String, E> files = new HashMap<>();
|
||||||
|
|
||||||
public Dir(String name) {
|
public Dir(String name, String fullName) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.fullName = fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRoot() {
|
public boolean isRoot() {
|
||||||
@@ -134,6 +224,11 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
|
|||||||
return name;
|
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() {
|
public @Nullable E getEntry() {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl.util.tree;
|
|||||||
|
|
||||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||||
import kala.compress.archivers.tar.TarArchiveReader;
|
import kala.compress.archivers.tar.TarArchiveReader;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributeView;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,9 +100,22 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
|
|||||||
this.shutdownHook = null;
|
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
|
@Override
|
||||||
public InputStream getInputStream(TarArchiveEntry entry) throws IOException {
|
public InputStream getInputStream(TarArchiveEntry entry) throws IOException {
|
||||||
return file.getInputStream(entry);
|
return reader.getInputStream(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -121,7 +136,7 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
|
|||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
try {
|
try {
|
||||||
file.close();
|
reader.close();
|
||||||
} finally {
|
} finally {
|
||||||
if (tempFile != null) {
|
if (tempFile != null) {
|
||||||
Runtime.getRuntime().removeShutdownHook(shutdownHook);
|
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.ZipArchiveEntry;
|
||||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
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
|
* @author Glavo
|
||||||
@@ -51,12 +59,53 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
|
|||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
if (closeReader)
|
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
|
@Override
|
||||||
public InputStream getInputStream(ZipArchiveEntry entry) throws IOException {
|
public InputStream getInputStream(ZipArchiveEntry entry) throws IOException {
|
||||||
return getFile().getInputStream(entry);
|
return getReader().getInputStream(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -66,7 +115,7 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getLink(ZipArchiveEntry entry) throws IOException {
|
public String getLink(ZipArchiveEntry entry) throws IOException {
|
||||||
return getFile().getUnixSymlink(entry);
|
return getReader().getUnixSymlink(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user