From e1fd530614be803f1a826704a1bd3168ad527ddf Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 9 Sep 2025 16:25:52 +0800 Subject: [PATCH] add VirtualFile --- .../hutool/v7/core/io/file/FileWrapper.java | 2 + .../cn/hutool/v7/core/io/file/PathUtil.java | 33 +- .../hutool/v7/core/io/file/VirtualFile.java | 110 +++++++ .../hutool/v7/core/io/file/VirtualPath.java | 303 ++++++++++++++++++ .../v7/core/io/file/VirtualFileTest.java | 75 +++++ .../v7/core/io/file/VirtualPathTest.java | 206 ++++++++++++ .../v7/extra/compress/archiver/Archiver.java | 17 + .../compress/archiver/SevenZArchiver.java | 56 +++- .../compress/archiver/StreamArchiver.java | 52 +++ 9 files changed, 844 insertions(+), 10 deletions(-) create mode 100644 hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualFile.java create mode 100644 hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualPath.java create mode 100644 hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualFileTest.java create mode 100644 hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualPathTest.java diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/io/file/FileWrapper.java b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/FileWrapper.java index bbd608c9d..feff9bdb0 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/io/file/FileWrapper.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/FileWrapper.java @@ -22,6 +22,7 @@ import cn.hutool.v7.core.util.CharsetUtil; import cn.hutool.v7.core.util.ObjUtil; import java.io.File; +import java.io.Serial; import java.io.Serializable; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -32,6 +33,7 @@ import java.nio.charset.StandardCharsets; * @author Looly */ public class FileWrapper implements Wrapper, Serializable { + @Serial private static final long serialVersionUID = 1L; /** diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/io/file/PathUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/PathUtil.java index c1bf573e3..d39f28f72 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/io/file/PathUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/PathUtil.java @@ -492,12 +492,24 @@ public class PathUtil { * @throws IORuntimeException IO异常 */ public static BasicFileAttributes getAttributes(final Path path, final boolean isFollowLinks) throws IORuntimeException { + return getAttributes(path, getLinkOptions(isFollowLinks)); + } + + /** + * 获取文件属性 + * + * @param path 文件路径{@link Path} + * @param options {@link LinkOption} + * @return {@link BasicFileAttributes} + * @throws IORuntimeException IO异常 + */ + public static BasicFileAttributes getAttributes(final Path path, final LinkOption... options) throws IORuntimeException { if (null == path) { return null; } try { - return Files.readAttributes(path, BasicFileAttributes.class, getLinkOptions(isFollowLinks)); + return Files.readAttributes(path, BasicFileAttributes.class, options); } catch (final IOException e) { throw new IORuntimeException(e); } @@ -506,15 +518,16 @@ public class PathUtil { /** * 获得输入流 * - * @param path Path + * @param path Path + * @param options {@link OpenOption} * @return 输入流 * @throws IORuntimeException 文件未找到 * @since 4.0.0 */ - public static BufferedInputStream getInputStream(final Path path) throws IORuntimeException { + public static BufferedInputStream getInputStream(final Path path, final OpenOption... options) throws IORuntimeException { final InputStream in; try { - in = Files.newInputStream(path); + in = Files.newInputStream(path, options); } catch (final IOException e) { throw new IORuntimeException(e); } @@ -524,13 +537,14 @@ public class PathUtil { /** * 获得一个文件读取器 * - * @param path 文件Path + * @param path 文件Path + * @param options {@link OpenOption} * @return BufferedReader对象 * @throws IORuntimeException IO异常 * @since 4.0.0 */ - public static BufferedReader getUtf8Reader(final Path path) throws IORuntimeException { - return getReader(path, CharsetUtil.UTF_8); + public static BufferedReader getUtf8Reader(final Path path, final OpenOption... options) throws IORuntimeException { + return getReader(path, CharsetUtil.UTF_8, options); } /** @@ -538,12 +552,13 @@ public class PathUtil { * * @param path 文件Path * @param charset 字符集 + * @param options {@link OpenOption} * @return BufferedReader对象 * @throws IORuntimeException IO异常 * @since 4.0.0 */ - public static BufferedReader getReader(final Path path, final Charset charset) throws IORuntimeException { - return IoUtil.toReader(getInputStream(path), charset); + public static BufferedReader getReader(final Path path, final Charset charset, final OpenOption... options) throws IORuntimeException { + return IoUtil.toReader(getInputStream(path, options), charset); } /** diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualFile.java b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualFile.java new file mode 100644 index 000000000..938e06a8a --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualFile.java @@ -0,0 +1,110 @@ +package cn.hutool.v7.core.io.file; + +import cn.hutool.v7.core.io.resource.Resource; + +import java.io.File; +import java.io.Serial; + +/** + * 虚拟文件类,继承自File,用于在内存中模拟文件 + */ +public class VirtualFile extends File { + @Serial + private static final long serialVersionUID = 1L; + + private final Resource content; + + /** + * 构造一个虚拟文件 + * + * @param pathname 文件路径 + * @param content 文件内容 + */ + public VirtualFile(final String pathname, final Resource content) { + super(pathname); + this.content = content; + } + + /** + * 构造一个虚拟文件 + * + * @param parent 父路径 + * @param child 子文件名 + * @param content 文件内容 + */ + public VirtualFile(final String parent, final String child, final Resource content) { + super(parent, child); + this.content = content; + } + + /** + * 构造一个虚拟文件 + * + * @param parent 父文件 + * @param child 子文件名 + * @param content 文件内容 + */ + public VirtualFile(final File parent, final String child, final Resource content) { + super(parent, child); + this.content = content; + } + + /** + * 获取文件内容,{@code null}表示无此文件 + * + * @return 文件内容 + */ + public Resource getContent() { + return this.content; + } + + /** + * 获取文件内容,{@code null}表示无此文件 + * + * @return 文件内容 + */ + public byte[] getBytes() { + return null != this.content ? this.content.readBytes() : null; + } + + @Override + public boolean exists() { + return null != this.content; + } + + @Override + public boolean isFile() { + return true; + } + + @Override + public boolean isDirectory() { + return false; + } + + @Override + public long length() { + return null != this.content ? this.content.size() : 0L; + } + + @Override + public boolean canRead() { + return exists(); + } + + @Override + public boolean canWrite() { + return false; + } + + @Override + public boolean canExecute() { + return false; + } + + @Override + public long lastModified() { + return System.currentTimeMillis(); + } +} + diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualPath.java b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualPath.java new file mode 100644 index 000000000..1e5ca099b --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/v7/core/io/file/VirtualPath.java @@ -0,0 +1,303 @@ +package cn.hutool.v7.core.io.file; + +import cn.hutool.v7.core.io.resource.Resource; +import cn.hutool.v7.core.text.CharUtil; +import cn.hutool.v7.core.text.StrUtil; +import cn.hutool.v7.core.text.split.SplitUtil; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.*; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * 虚拟路径类,实现Path接口,用于在内存中模拟文件路径 + */ +public class VirtualPath implements Path { + + private final String path; + private final Resource content; + + /** + * 构造一个虚拟路径 + * + * @param path 路径字符串 + * @param content 路径对应的内容 + */ + public VirtualPath(final String path, final Resource content) { + this.path = path; + this.content = content; + } + + /** + * 获取路径内容 + * + * @return 内容 + */ + public Resource getContent() { + return this.content; + } + + /** + * 获取文件内容,{@code null}表示无此文件 + * + * @return 文件内容 + */ + public byte[] getBytes() { + return null != this.content ? this.content.readBytes() : null; + } + + @Override + public FileSystem getFileSystem() { + throw new UnsupportedOperationException("VirtualPath does not support FileSystem operations"); + } + + @Override + public boolean isAbsolute() { + return false; + } + + @Override + public Path getRoot() { + return null; + } + + @Override + public Path getFileName() { + final int index = path.lastIndexOf(CharUtil.SLASH); + if (index == -1) { + return new VirtualPath(path, content); + } + return new VirtualPath(path.substring(index + 1), content); + } + + @Override + public Path getParent() { + final int index = path.lastIndexOf(CharUtil.SLASH); + if (index == -1) { + return null; + } + return new VirtualPath(path.substring(0, index), null); + } + + @Override + public int getNameCount() { + if(StrUtil.isEmpty(path)){ + // ""表示一个有效名称 + return 1; + } + if(StrUtil.equals(path, StrUtil.SLASH)){ + // /表示根路径,无名称 + return 0; + } + // 根路径不算名称 + return StrUtil.count(path, StrUtil.SLASH); + } + + @Override + public Path getName(final int index) { + if (index < 0) { + throw new IllegalArgumentException("index must be >= 0"); + } + + final List parts = SplitUtil.splitTrim(path, StrUtil.SLASH); + if (index >= parts.size()) { + throw new IllegalArgumentException("index exceeds name count"); + } + + return new VirtualPath(parts.get(index), index == parts.size() - 1 ? content : null); + } + + @Override + public Path subpath(final int beginIndex, final int endIndex) { + if (beginIndex < 0 || endIndex <= beginIndex) { + throw new IllegalArgumentException("beginIndex or endIndex is invalid"); + } + + final List parts = SplitUtil.splitTrim(path, StrUtil.SLASH); + if (endIndex > parts.size()) { + throw new IllegalArgumentException("endIndex exceeds name count"); + } + + final StringBuilder sb = new StringBuilder(); + for (int i = beginIndex; i < endIndex; i++) { + if (!sb.isEmpty()) { + sb.append(CharUtil.SLASH); + } + sb.append(parts.get(i)); + } + + return new VirtualPath(sb.toString(), endIndex == parts.size() ? content : null); + } + + @Override + public boolean startsWith(final Path other) { + if (!(other instanceof final VirtualPath otherPath)) { + return false; + } + return this.path.startsWith(otherPath.path); + } + + @Override + public boolean startsWith(final String other) { + return this.path.startsWith(other); + } + + @Override + public boolean endsWith(final Path other) { + if (!(other instanceof final VirtualPath otherPath)) { + return false; + } + return this.path.endsWith(otherPath.path); + } + + @Override + public boolean endsWith(final String other) { + return this.path.endsWith(other); + } + + @Override + public Path normalize() { + return this; + } + + @Override + public Path resolve(final Path other) { + if (other.isAbsolute()) { + return other; + } + if (other.toString().isEmpty()) { + return this; + } + final String newPath = this.path + "/" + other; + return new VirtualPath(newPath, other instanceof VirtualPath ? ((VirtualPath) other).content : null); + } + + @Override + public Path resolve(final String other) { + if (other.isEmpty()) { + return this; + } + final String newPath = this.path + StrUtil.SLASH + other; + return new VirtualPath(newPath, null); + } + + @Override + public Path resolveSibling(final Path other) { + if (other == null) { + throw new NullPointerException("other cannot be null"); + } + final Path parent = getParent(); + return (parent == null) ? other : parent.resolve(other); + } + + @Override + public Path resolveSibling(final String other) { + if (other == null) { + throw new NullPointerException("other cannot be null"); + } + final Path parent = getParent(); + return (parent == null) ? new VirtualPath(other, null) : parent.resolve(other); + } + + @Override + public Path relativize(final Path other) { + if (!(other instanceof final VirtualPath otherPath)) { + throw new IllegalArgumentException("other must be a VirtualPath"); + } + + if (this.path.isEmpty()) { + return otherPath; + } + + if (otherPath.path.startsWith(this.path + "/")) { + return new VirtualPath(otherPath.path.substring(this.path.length() + 1), otherPath.content); + } + + return otherPath; + } + + @Override + public URI toUri() { + throw new UnsupportedOperationException("VirtualPath does not support URI conversion"); + } + + @Override + public Path toAbsolutePath() { + return this; + } + + @Override + public Path toRealPath(final LinkOption... options) { + return this; + } + + @Override + public File toFile() { + return new VirtualFile(path, content); + } + + @Override + public WatchKey register(final WatchService watcher, final WatchEvent.Kind[] events, final WatchEvent.Modifier... modifiers) throws IOException { + throw new UnsupportedOperationException("VirtualPath does not support watch service"); + } + + @Override + public WatchKey register(final WatchService watcher, final WatchEvent.Kind... events) throws IOException { + throw new UnsupportedOperationException("VirtualPath does not support watch service"); + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private int index = 0; + private final List parts = SplitUtil.splitTrim(path, StrUtil.SLASH); + + @Override + public boolean hasNext() { + return index < parts.size(); + } + + @Override + public Path next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return new VirtualPath(parts.get(index++), index == parts.size() ? content : null); + } + }; + } + + @Override + public int compareTo(final Path other) { + if (!(other instanceof final VirtualPath otherPath)) { + throw new ClassCastException("Cannot compare VirtualPath with " + other.getClass().getName()); + } + return this.path.compareTo(otherPath.path); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof final VirtualPath otherPath)) { + return false; + } + return this.path.equals(otherPath.path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + @Override + public String toString() { + return path; + } +} + diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualFileTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualFileTest.java new file mode 100644 index 000000000..3fa71c1a2 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualFileTest.java @@ -0,0 +1,75 @@ +package cn.hutool.v7.core.io.file; + +import cn.hutool.v7.core.io.resource.BytesResource; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.*; + +public class VirtualFileTest { + @Test + public void testConstructorWithParentChildAndContent() { + final byte[] content = "Hello World".getBytes(); + final VirtualFile virtualFile = new VirtualFile("/tmp", "test.txt", new BytesResource(content)); + + assertEquals("/tmp", virtualFile.getParent().replace(File.separator, "/")); + assertEquals("test.txt", virtualFile.getName()); + assertArrayEquals(content, virtualFile.getBytes()); + } + + @Test + public void testConstructorWithStringAndContent() { + final byte[] content = "Hello World".getBytes(); + final VirtualFile virtualFile = new VirtualFile("/tmp/test.txt", new BytesResource(content)); + + assertEquals("/tmp/test.txt", virtualFile.getPath().replace(File.separator, "/")); + assertArrayEquals(content, virtualFile.getBytes()); + } + + @Test + public void testConstructorWithFileParentAndContent() { + final File parent = new File("/tmp"); + final byte[] content = "Hello World".getBytes(); + final VirtualFile virtualFile = new VirtualFile(parent, "test.txt", new BytesResource(content)); + + assertEquals("/tmp", virtualFile.getParent().replace(File.separator, "/")); + assertEquals("test.txt", virtualFile.getName()); + assertArrayEquals(content, virtualFile.getBytes()); + } + + @Test + public void testFileProperties() { + final byte[] content = "Hello World".getBytes(); + final VirtualFile virtualFile = new VirtualFile("/tmp/test.txt", new BytesResource(content)); + + assertTrue(virtualFile.exists()); + assertTrue(virtualFile.isFile()); + assertFalse(virtualFile.isDirectory()); + assertEquals(content.length, virtualFile.length()); + assertTrue(virtualFile.canRead()); + assertFalse(virtualFile.canWrite()); + assertFalse(virtualFile.canExecute()); + assertTrue(virtualFile.lastModified() > 0); + } + + @Test + public void testContentImmutability() { + final byte[] originalContent = "Hello World".getBytes(); + final VirtualFile virtualFile = new VirtualFile("/tmp/test.txt", new BytesResource(originalContent)); + + final byte[] contentFromMethod = virtualFile.getBytes(); + // 修改获取到的内容不应该影响原始内容 + contentFromMethod[0] = 'h'; + + // 重新获取内容,应该和原始内容一致 + final byte[] newContentFromMethod = virtualFile.getBytes(); + assertArrayEquals(originalContent, newContentFromMethod); + } + + @Test + public void testNullContent() { + final VirtualFile virtualFile = new VirtualFile("/tmp/test.txt", null); + assertNull(virtualFile.getBytes()); + } +} diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualPathTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualPathTest.java new file mode 100644 index 000000000..03a54f2c1 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/v7/core/io/file/VirtualPathTest.java @@ -0,0 +1,206 @@ +package cn.hutool.v7.core.io.file; + +import cn.hutool.v7.core.io.resource.BytesResource; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * VirtualPath单元测试 + */ +public class VirtualPathTest { + + @Test + public void testConstructor() { + final byte[] content = "Hello World".getBytes(); + final VirtualPath virtualPath = new VirtualPath("/tmp/test.txt", new BytesResource(content)); + + assertEquals("/tmp/test.txt", virtualPath.toString()); + assertArrayEquals(content, virtualPath.getBytes()); + } + + @Test + public void testGetFileName() { + final byte[] content = "Hello World".getBytes(); + final VirtualPath virtualPath = new VirtualPath("/tmp/test.txt", new BytesResource(content)); + final Path fileName = virtualPath.getFileName(); + + assertEquals("test.txt", fileName.toString()); + assertArrayEquals(content, ((VirtualPath) fileName).getBytes()); + } + + @Test + public void testGetParent() { + final VirtualPath virtualPath = new VirtualPath("/tmp/subdir/test.txt", null); + final Path parent = virtualPath.getParent(); + + assertEquals("/tmp/subdir", parent.toString()); + assertNull(((VirtualPath) parent).getContent()); + } + + @Test + public void testGetNameCount() { + final String pathStr = "/tmp/subdir/test.txt"; + + final VirtualPath virtualPath = new VirtualPath(pathStr, null); + assertEquals(Paths.get(pathStr).getNameCount(), virtualPath.getNameCount()); + + final VirtualPath rootPath = new VirtualPath("/", null); + assertEquals(Paths.get("/").getNameCount(), rootPath.getNameCount()); + + final VirtualPath emptyPath = new VirtualPath("", null); + assertEquals(Paths.get("").getNameCount(), emptyPath.getNameCount()); + } + + @Test + public void testGetName() { + final byte[] content = "Hello World".getBytes(); + final VirtualPath virtualPath = new VirtualPath("/tmp/subdir/test.txt", new BytesResource(content)); + + final Path name0 = virtualPath.getName(0); + assertEquals("tmp", name0.toString()); + assertNull(((VirtualPath) name0).getBytes()); + + final Path name1 = virtualPath.getName(1); + assertEquals("subdir", name1.toString()); + assertNull(((VirtualPath) name1).getBytes()); + + final Path name2 = virtualPath.getName(2); + assertEquals("test.txt", name2.toString()); + assertArrayEquals(content, ((VirtualPath) name2).getBytes()); + } + + @Test + public void testSubpath() { + final VirtualPath virtualPath = new VirtualPath("/tmp/subdir/test.txt", null); + + final Path subpath = virtualPath.subpath(1, 3); + assertEquals("subdir/test.txt", subpath.toString()); + + final Path single = virtualPath.subpath(0, 1); + assertEquals("tmp", single.toString()); + } + + @Test + public void testStartsWith() { + final VirtualPath virtualPath = new VirtualPath("/tmp/subdir/test.txt", null); + + assertTrue(virtualPath.startsWith("/tmp")); + assertTrue(virtualPath.startsWith(new VirtualPath("/tmp", null))); + assertFalse(virtualPath.startsWith("/home")); + } + + @Test + public void testEndsWith() { + final VirtualPath virtualPath = new VirtualPath("/tmp/subdir/test.txt", null); + + assertTrue(virtualPath.endsWith("test.txt")); + assertTrue(virtualPath.endsWith(new VirtualPath("test.txt", null))); + assertFalse(virtualPath.endsWith("config.txt")); + } + + @Test + public void testResolve() { + final VirtualPath virtualPath = new VirtualPath("/tmp", null); + + final Path resolved = virtualPath.resolve("test.txt"); + assertEquals("/tmp/test.txt", resolved.toString()); + + final Path resolvedPath = virtualPath.resolve(new VirtualPath("subdir/test.txt", new BytesResource("content".getBytes()))); + assertEquals("/tmp/subdir/test.txt", resolvedPath.toString()); + } + + @Test + public void testResolveSibling() { + final VirtualPath virtualPath = new VirtualPath("/tmp/test.txt", null); + + final Path resolved = virtualPath.resolveSibling("config.txt"); + assertEquals("/tmp/config.txt", resolved.toString()); + } + + @Test + public void testRelativize() { + final VirtualPath path1 = new VirtualPath("/tmp/subdir", null); + final VirtualPath path2 = new VirtualPath("/tmp/subdir/test.txt", null); + + final Path relativized = path1.relativize(path2); + assertEquals("test.txt", relativized.toString()); + } + + @Test + public void testToFile() { + final byte[] content = "Hello World".getBytes(); + final VirtualPath virtualPath = new VirtualPath("/tmp/test.txt", new BytesResource( content)); + + final File file = virtualPath.toFile(); + assertInstanceOf(VirtualFile.class, file); + assertEquals("/tmp/test.txt", file.getPath().replace(File.separator, "/")); + assertArrayEquals(content, ((VirtualFile) file).getBytes()); + } + + @Test + public void testIterator() { + final VirtualPath virtualPath = new VirtualPath("/tmp/subdir/test.txt", new BytesResource("content".getBytes())); + final Iterator iterator = virtualPath.iterator(); + + assertTrue(iterator.hasNext()); + assertEquals("tmp", iterator.next().toString()); + + assertTrue(iterator.hasNext()); + assertEquals("subdir", iterator.next().toString()); + + assertTrue(iterator.hasNext()); + final Path last = iterator.next(); + assertEquals("test.txt", last.toString()); + assertArrayEquals("content".getBytes(), ((VirtualPath) last).getBytes()); + + assertFalse(iterator.hasNext()); + } + + @Test + public void testCompareTo() { + final VirtualPath path1 = new VirtualPath("/tmp/a.txt", null); + final VirtualPath path2 = new VirtualPath("/tmp/b.txt", null); + + assertTrue(path1.compareTo(path2) < 0); + assertTrue(path2.compareTo(path1) > 0); + assertEquals(0, path1.compareTo(new VirtualPath("/tmp/a.txt", null))); + } + + @Test + public void testEqualsAndHashCode() { + final VirtualPath path1 = new VirtualPath("/tmp/test.txt", null); + final VirtualPath path2 = new VirtualPath("/tmp/test.txt", null); + final VirtualPath path3 = new VirtualPath("/tmp/config.txt", null); + + assertEquals(path1, path2); + assertNotEquals(path1, path3); + assertEquals(path1.hashCode(), path2.hashCode()); + } + + @Test + public void testContentImmutability() { + final byte[] originalContent = "Hello World".getBytes(); + final VirtualPath virtualPath = new VirtualPath("/tmp/test.txt", new BytesResource(originalContent)); + + final byte[] contentFromMethod = virtualPath.getBytes(); + // 修改获取到的内容不应该影响原始内容 + contentFromMethod[0] = 'h'; + + // 重新获取内容,应该和原始内容一致 + final byte[] newContentFromMethod = virtualPath.getBytes(); + assertArrayEquals(originalContent, newContentFromMethod); + } + + @Test + public void testNullContent() { + final VirtualPath virtualPath = new VirtualPath("/tmp/test.txt", null); + assertNull(virtualPath.getContent()); + } +} + diff --git a/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/Archiver.java b/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/Archiver.java index 65266caf7..2df2e401e 100644 --- a/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/Archiver.java +++ b/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/Archiver.java @@ -18,6 +18,8 @@ package cn.hutool.v7.extra.compress.archiver; import java.io.Closeable; import java.io.File; +import java.nio.file.LinkOption; +import java.nio.file.Path; import java.util.function.Function; import java.util.function.Predicate; @@ -73,6 +75,21 @@ public interface Archiver extends Closeable { */ Archiver add(File file, String path, Function fileNameEditor, Predicate filter); + /** + * 将文件或目录加入归档包,目录采取递归读取方式按照层级加入 + * + * @param file 文件或目录 + * @param path 文件或目录的初始路径,null表示位于根路径 + * @param fileNameEditor 文件名编辑器 + * @param filter 文件过滤器,指定哪些文件或目录可以加入,{@link Predicate#test(Object)}为{@code true}保留,null表示全部加入 + * @param options 链接选项 + * @return this + * @since 7.0.0 + */ + default Archiver add(final Path file, final String path, final Function fileNameEditor, final Predicate filter, final LinkOption... options){ + return add(file.toFile(), path, fileNameEditor, (f-> filter.test(f.toPath()))); + } + /** * 结束已经增加的文件归档,此方法不会关闭归档流,可以继续添加文件 * diff --git a/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/SevenZArchiver.java b/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/SevenZArchiver.java index 6426a887e..aaee93e40 100644 --- a/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/SevenZArchiver.java +++ b/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/SevenZArchiver.java @@ -25,11 +25,13 @@ import cn.hutool.v7.extra.compress.CompressUtil; import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; +import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.function.Function; import java.util.function.Predicate; @@ -106,6 +108,16 @@ public class SevenZArchiver implements Archiver { return this; } + @Override + public SevenZArchiver add(final Path file, final String path, final Function fileNameEditor, final Predicate filter, final LinkOption... options) { + try { + addInternal(file, path, fileNameEditor, filter, options); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + return this; + } + @Override public SevenZArchiver finish() { try { @@ -116,6 +128,7 @@ public class SevenZArchiver implements Archiver { return this; } + @SuppressWarnings("resource") @Override public void close() { try { @@ -161,7 +174,48 @@ public class SevenZArchiver implements Archiver { } else { if (file.isFile()) { // 文件直接写入 - out.write(FileUtil.readBytes(file)); + try (final BufferedInputStream in = FileUtil.getInputStream(file)) { + out.write(in); + } + //out.write(FileUtil.readBytes(file)); + } + out.closeArchiveEntry(); + } + } + + /** + * 将文件或目录加入归档包,目录采取递归读取方式按照层级加入 + * + * @param file 文件或目录 + * @param path 文件或目录的初始路径,null表示位于根路径 + * @param fileNameEditor 文件名编辑器 + * @param filter 文件过滤器,指定哪些文件或目录可以加入,当{@link Predicate#test(Object)}为{@code true}保留,null表示保留全部 + * @param options 链接选项 + */ + private void addInternal(final Path file, final String path, final Function fileNameEditor, final Predicate filter, final LinkOption... options) throws IOException { + if (null != filter && !filter.test(file)) { + return; + } + final SevenZOutputFile out = this.sevenZOutputFile; + + final String entryName = CompressUtil.getEntryName(PathUtil.getName(file), path, fileNameEditor); + out.putArchiveEntry(out.createArchiveEntry(file, entryName)); + + if (PathUtil.isDirectory(file)) { + // 目录遍历写入 + final Path[] files = PathUtil.listFiles(file, null); + if (ArrayUtil.isNotEmpty(files)) { + for (final Path childFile : files) { + addInternal(childFile, entryName, fileNameEditor, filter); + } + } + } else { + if (Files.isRegularFile(file, options)) { + // 文件直接写入 + try (final BufferedInputStream in = PathUtil.getInputStream(file)) { + out.write(in); + } + //out.write(FileUtil.readBytes(file)); } out.closeArchiveEntry(); } diff --git a/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/StreamArchiver.java b/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/StreamArchiver.java index f9b7f9800..5db5f0455 100644 --- a/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/StreamArchiver.java +++ b/hutool-extra/src/main/java/cn/hutool/v7/extra/compress/archiver/StreamArchiver.java @@ -16,12 +16,14 @@ package cn.hutool.v7.extra.compress.archiver; +import cn.hutool.v7.core.io.file.PathUtil; import cn.hutool.v7.extra.compress.CompressUtil; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream; +import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; import cn.hutool.v7.core.array.ArrayUtil; @@ -31,10 +33,14 @@ import cn.hutool.v7.core.io.file.FileUtil; import cn.hutool.v7.core.text.StrUtil; import cn.hutool.v7.extra.compress.CompressException; +import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; import java.util.function.Function; import java.util.function.Predicate; @@ -132,6 +138,16 @@ public class StreamArchiver implements Archiver { return this; } + @Override + public StreamArchiver add(final Path file, final String path, final Function fileNameEditor, final Predicate filter, final LinkOption... options) { + try { + addInternal(file, path, fileNameEditor, filter, options); + } catch (final IOException e) { + throw new IORuntimeException(e); + } + return this; + } + /** * 结束已经增加的文件归档,此方法不会关闭归档流,可以继续添加文件 * @@ -195,4 +211,40 @@ public class StreamArchiver implements Archiver { out.closeArchiveEntry(); } } + + /** + * 将文件或目录加入归档包,目录采取递归读取方式按照层级加入 + * + * @param file 文件或目录 + * @param path 文件或目录的初始路径,null表示位于根路径 + * @param fileNameEditor 文件名编辑器 + * @param filter 文件过滤器,指定哪些文件或目录可以加入,当{@link Predicate#test(Object)}为{@code true}保留,null表示保留全部 + * @param options 链接选项 + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private void addInternal(final Path file, final String path, final Function fileNameEditor, final Predicate filter, final LinkOption... options) throws IOException { + if (null != filter && !filter.test(file)) { + return; + } + final ArchiveOutputStream out = this.out; + + final String entryName = CompressUtil.getEntryName(PathUtil.getName(file), path, fileNameEditor); + out.putArchiveEntry(out.createArchiveEntry(file, entryName)); + + if (PathUtil.isDirectory(file)) { + // 目录遍历写入 + final Path[] files = PathUtil.listFiles(file, null); + if (ArrayUtil.isNotEmpty(files)) { + for (final Path childFile : files) { + addInternal(childFile, entryName, fileNameEditor, filter); + } + } + } else { + if (Files.isRegularFile(file, options)) { + // 文件直接写入 + PathUtil.copy(file, out); + } + out.closeArchiveEntry(); + } + } }