diff --git a/.gitignore b/.gitignore index c6d98d1..0ff1a91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,98 +1,40 @@ -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Maven target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +/test diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7ace6ad --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,26 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..bada8b5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..325f87f --- /dev/null +++ b/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + com.imyeyu.io + timi-io + 0.0.1 + jar + + + true + 21 + 21 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + UTF-8 + + + + + + + + com.imyeyu.utils + timi-utils + 0.0.1 + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + test + + + diff --git a/src/main/java/com/imyeyu/io/IO.java b/src/main/java/com/imyeyu/io/IO.java new file mode 100644 index 0000000..12f0310 --- /dev/null +++ b/src/main/java/com/imyeyu/io/IO.java @@ -0,0 +1,1056 @@ +package com.imyeyu.io; + +import com.imyeyu.java.bean.Callback; +import com.imyeyu.java.bean.CallbackArg; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.utils.Decoder; +import com.imyeyu.utils.OS; +import com.imyeyu.utils.Text; + +import javax.naming.NoPermissionException; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; + +/** + * 文件和数据流操作 + * + * @author 夜雨 + * @version 2021-02-13 10:15 + */ +public class IO implements OS.FileSystem { + + /** + * 适应路径,在不确定路径结尾是否存在分隔符时总是补充分隔符 + * + * @param path 路径 + * @return 补充分隔符的路径 + */ + public static String fitPath(String path) { + return path.endsWith(SEP) ? path : path + SEP; + } + + /** + * 获取简易的文件名,没有格式 + * + * @param file 文件 + * @return 文件名 + */ + public static String simpleFileName(File file) { + if (file == null) { + return null; + } + if (file.isDirectory()) { + return file.getName(); + } + String name = file.getName(); + int dot = name.lastIndexOf("."); + if (dot != -1) { + return name.substring(0, dot); + } + return name; + } + + /** + * 获取文件扩展名 + * + * @param file 文件 + * @return 扩展名 + */ + public static String fileExtension(File file) { + if (file == null) { + return null; + } + if (file.isDirectory()) { + return ""; + } + String ext = file.getName(); + int dot = ext.lastIndexOf("."); + if (dot != -1) { + return ext.substring(dot + 1); + } + return ""; + } + + /** + * 运行程序(.jar)所在磁盘的绝对路径,在 debug 环境中为类路径 + * + * @param clazz 调用方类路径 + * @return 绝对路径 + */ + public static String getJarAbsolutePath(Class clazz) { + return new File(Decoder.url(clazz.getProtectionDomain().getCodeSource().getLocation().getPath())).getAbsolutePath(); + } + + /** + * 使用该路径创建文件夹 + * + * @param path 路径 + * @return 文件夹对象 + * @throws NoPermissionException 无权限时异常 + */ + public static synchronized File dir(String path) throws NoPermissionException { + File dir = new File(path); + if (!dir.exists()) { + if (OS.isValidFileName(dir.getName())) { + if (!dir.mkdirs()) { + throw new NoPermissionException("Can not create out put directory: " + dir.getAbsolutePath()); + } + } else { + String msg = "Illegal directory name: %s, not allow char: %s"; + if (OS.IS_WINDOWS) { + throw new IllegalArgumentException(msg.formatted(dir.getName(), Arrays.asList(OS.INVALID_WINDOWS_SPECIFIC_CHARS))); + } else { + throw new IllegalArgumentException(msg.formatted(dir.getName(), Arrays.asList(OS.INVALID_UNIX_SPECIFIC_CHARS))); + } + } + } + return dir; + } + + /** + * 获取文件对象,如果不存在则创建该文件 + * + * @param parent 所属文件夹 + * @param name 文件名 + * @return 文件 + * @throws NoPermissionException 权限异常 + * @throws IOException 数据流异常 + */ + public static File file(File parent, String name) throws NoPermissionException, IOException { + return file(IO.fitPath(parent.getAbsolutePath()) + name, null); + } + + /** + * 获取文件对象,如果不存在则创建该文件 + * + * @param path 文件路径 + * @return 文件 + * @throws NoPermissionException 权限异常 + * @throws IOException 数据流异常 + */ + public static File file(String path) throws NoPermissionException, IOException { + return file(path, null); + } + + /** + * 获取文件对象,如果不存在则创建该文件 + * + * @param path 文件路径 + * @param onCreatedFile 创建回调,不创建时不触发 + * @return 文件 + * @throws NoPermissionException 权限异常 + * @throws IOException 数据流异常 + */ + public static synchronized File file(String path, CallbackArg onCreatedFile) throws NoPermissionException, IOException { + File file = new File(path); + // 文件夹 + File parent = file.getParentFile(); + if (parent != null && !parent.exists()) { + dir(parent.getAbsolutePath()); + } + boolean isCreated = false; + // 文件 + if (!file.exists()) { + if (OS.isValidFileName(file.getName())) { + if (!(isCreated = file.createNewFile())) { + throw new NoPermissionException("Can not create the file: " + file.getAbsolutePath()); + } + } else { + String msg = "Illegal directory name: %s, not allow char: %s"; + if (OS.IS_WINDOWS) { + throw new IllegalArgumentException(msg.formatted(file.getName(), Arrays.asList(OS.INVALID_WINDOWS_SPECIFIC_CHARS))); + } else { + throw new IllegalArgumentException(msg.formatted(file.getName(), Arrays.asList(OS.INVALID_UNIX_SPECIFIC_CHARS))); + } + } + } + // 权限 + boolean canExecutable = file.setExecutable(true, false); + boolean canReadable = file.setReadable(true, false); + boolean canWritable = file.setWritable(true, false); + if (!canExecutable || !canReadable || !canWritable) { + throw new NoPermissionException("Can not write or read the file: " + file.getAbsolutePath()); + } + // 回调 + if (onCreatedFile != null && isCreated) { + onCreatedFile.handler(file); + } + return file; + } + + /** + * 重命名文件 + * + * @param file 文件 + * @param name 文件名 + * @return 重命名后的文件对象 + */ + public static File rename(File file, String name) { + if (OS.isValidFileName(name)) { + File resultFile = new File(fitPath(file.getParent()) + name); + if (file.renameTo(resultFile) && resultFile.exists()) { + return resultFile; + } + throw new TimiException(TimiCode.RESULT_BAD, "rename file fail: from " + file.getAbsolutePath() + ", to " + resultFile.getAbsolutePath()); + } else { + String msg = "Illeagl file name: %s, not allow char: %s"; + if (OS.IS_WINDOWS) { + throw new IllegalArgumentException(msg.formatted(name, Arrays.asList(OS.INVALID_WINDOWS_SPECIFIC_CHARS))); + } else { + throw new IllegalArgumentException(msg.formatted(name, Arrays.asList(OS.INVALID_UNIX_SPECIFIC_CHARS))); + } + } + } + + /** + * 写入字节到文件 + * + * @param file 文件 + * @param bytes 字节 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static void toFile(File file, byte[] bytes) throws IOException, NoPermissionException { + OutputStream os = IO.getOutputStream(file); + os.write(bytes); + os.flush(); + os.close(); + } + + /** + * 写入字符串到文件 + * + * @param file 文件 + * @param data 字符串内容 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static void toFile(File file, String data) throws IOException, NoPermissionException { + toFile(file, data, StandardCharsets.UTF_8); + } + + /** + * 写入字符串到文件 + * + * @param file 文件 + * @param data 字符串内容 + * @param charset 字符编码 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static void toFile(File file, String data, String charset) throws IOException, NoPermissionException { + toFile(file, new ByteArrayInputStream(data.getBytes(charset)), null); + } + + /** + * 写入字符串到文件 + * + * @param file 文件 + * @param data 字符串内容 + * @param charset 字符编码 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static void toFile(File file, String data, Charset charset) throws IOException, NoPermissionException { + toFile(file, new ByteArrayInputStream(data.getBytes(charset)), null); + } + + /** + * 写入数据流到文件 + * + * @param absFile 文件绝对路径 + * @param is 输入流 + * @return 文件对象 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static File toFile(String absFile, InputStream is) throws IOException, NoPermissionException { + File file = new File(absFile); + toFile(file, is, null); + return file; + } + + /** + * 写入数据流到文件 + * + * @param file 文件 + * @param is 输入流 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static void toFile(File file, InputStream is) throws IOException, NoPermissionException { + toFile(file, is, null); + } + + /** + * 写入数据流到文件 + * + * @param file 文件 + * @param is 输入流 + * @param onWrote 写入文件回调,参数为已写入数据量,返回 true 时继续传输,否则中断流传输 + * @return true 为正常传输完成,否则发生中断传输 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static boolean toFile(File file, InputStream is, OnWriteCallback onWrote) throws IOException, NoPermissionException { + file = file(file.getAbsoluteFile().getParent() + SEP + file.getName()); + return toOutputStream(is, new FileOutputStream(file), onWrote); + } + + /** + * 序列化对象到文件 + * + * @param file 文件 + * @param object 可序列化对象 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static void toFile(File file, Object object) throws IOException, NoPermissionException { + toOutputStream(getOutputStream(file), object); + } + + /** + * 文件反序列化对象 + * + * @param file 文件 + * @param clazz 对象类型 + * @param 类型 + * @return 对象 + * @throws IOException IO 异常 + * @throws ClassNotFoundException 强制类型转换异常 + */ + @SuppressWarnings("unchecked") + public static T toObject(File file, Class clazz) throws IOException, ClassNotFoundException { + try (InputStream is = getInputStream(file); ObjectInputStream ois = new ObjectInputStream(is)) { + return (T) ois.readObject(); + } + } + + /** + * 读取文件为字符串(UTF-8) + * + * @param file 文件 + * @return 文件内容 + * @throws IOException IO 异常 + */ + public static String toString(File file) throws IOException { + return toString(getInputStream(file), null); + } + + /** + * 读取文件为字符串(UTF-8) + * + * @param file 文件 + * @param onRead 读取回调 + * @return 文件内容 + * @throws IOException IO 异常 + */ + public static String toString(File file, CallbackArg onRead) throws IOException { + return toString(getInputStream(file), onRead); + } + + /** + * 读取文件为字符串 + * + * @param file 文件 + * @param charset 编码类型 + * @return 文件内容 + * @throws IOException IO 异常 + */ + public static String toString(File file, Charset charset) throws IOException { + return toString(getInputStream(file), charset, null); + } + + /** + * 读取文件为字符串 + * + * @param file 文件 + * @param charset 编码类型 + * @param onWrote 读取回调 + * @return 文件内容 + * @throws IOException IO 异常 + */ + public static String toString(File file, Charset charset, CallbackArg onWrote) throws IOException { + return toString(getInputStream(file), charset, onWrote); + } + + /** + * 读取数据流为字符串(UTF-8) + * + * @param is 输入流 + * @return 字符串 + * @throws IOException IO 异常 + */ + public static String toString(InputStream is) throws IOException { + return toString(is, StandardCharsets.UTF_8, null); + } + + /** + * 读取数据流为字符串(UTF-8) + * + * @param is 输入流 + * @param onRead 读取回调 + * @return 字符串 + * @throws IOException IO 异常 + */ + public static String toString(InputStream is, CallbackArg onRead) throws IOException { + return toString(is, StandardCharsets.UTF_8, onRead); + } + + /** + * 读取数据流为字符串 + * + * @param is 输入流 + * @param charset 编码格式 + * @param onRead 读取回调 + * @return 字符串 + * @throws IOException IO 异常 + */ + public static String toString(InputStream is, Charset charset, CallbackArg onRead) throws IOException { + StringBuilder sb = new StringBuilder(); + BufferedInputStream bis = new BufferedInputStream(is); + + InputStreamReader isr = new InputStreamReader(bis, charset); + + char[] buffer = new char[4096]; + int l; + if (onRead == null) { + while ((l = isr.read(buffer)) != -1) { + sb.append(buffer, 0, l); + } + } else { + String string; + while ((l = isr.read(buffer)) != -1) { + string = new String(buffer, 0, l); + onRead.handler(string); + } + } + isr.close(); + bis.close(); + is.close(); + return sb.toString(); + } + + /** + * 获取文件数据流 + * + * @param path 文件路径 + * @return 数据流 + * @throws FileNotFoundException 找不到文件 + */ + public static InputStream getInputStream(String path) throws FileNotFoundException { + return getInputStream(new File(path)); + } + + /** + * 获取文件数据流 + * + * @param file 文件 + * @return 数据流 + * @throws FileNotFoundException 找不到文件 + */ + public static InputStream getInputStream(File file) throws FileNotFoundException { + return new FileInputStream(file); + } + + /** + * 获取文件随机访问对象 + * + * @param path 文件路径 + * @return 随机范文对象 + * @throws FileNotFoundException 找不到文件 + */ + public static RandomAccessFile getRandomAccessFile(String path) throws FileNotFoundException { + return getRandomAccessFile(new File(path)); + } + + /** + * 获取文件随机访问对象 + * + * @param file 文件 + * @return 随机访问对象 + * @throws FileNotFoundException 找不到文件 + */ + public static RandomAccessFile getRandomAccessFile(File file) throws FileNotFoundException { + return new RandomAccessFile(file, "rw"); + } + + /** + * 获取文件的输出流(如果不存在将会创建文件) + * + * @param path 文件路径 + * @return 输出流 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static OutputStream getOutputStream(String path) throws IOException, NoPermissionException { + return getOutputStream(new File(path)); + } + + /** + * 获取文件的输出流(如果不存在将会创建文件) + * + * @param file 文件 + * @return 输出流 + * @throws IOException IO 异常 + * @throws NoPermissionException 权限异常 + */ + public static OutputStream getOutputStream(File file) throws IOException, NoPermissionException { + return new FileOutputStream(file(file.getAbsolutePath())); + } + + /** + * 写入字符串到输出流 + * + * @param os 输出流 + * @param data 字符串 + * @throws IOException IO 异常 + */ + public static void toOutputStream(OutputStream os, String data) throws IOException { + toOutputStream(os, data.getBytes(StandardCharsets.UTF_8)); + } + + /** + * 写入字节到输出流 + * + * @param os 输出流 + * @param bytes 字节 + * @throws IOException IO 异常 + */ + public static void toOutputStream(OutputStream os, byte[] bytes) throws IOException { + toOutputStream(new ByteArrayInputStream(bytes), os); + } + + /** + * 读取文件并推到输出流 + * + * @param os 输出流 + * @param file 文件 + * @return true 为正常传输完成,否则发生中断传输 + * @throws IOException IO 异常 + */ + public static boolean toOutputStream(OutputStream os, File file) throws IOException { + return toOutputStream(getInputStream(file), os, null); + } + + /** + * 读取输入流并推输出流 + * + * @param is 输入流 + * @param os 输出流 + * @return true 为正常传输完成,否则发生中断传输 + * @throws IOException IO 异常 + */ + public static boolean toOutputStream(InputStream is, OutputStream os) throws IOException { + return toOutputStream(is, os, null, null); + } + + /** + * 读取输入流并推输出流 + * + * @param is 输入流 + * @param os 输出流 + * @param onWrote 写入回调,参数为已写入数据量,返回 true 时继续传输,否则中断流传输 + * @return true 为正常传输完成,否则发生中断传输 + * @throws IOException IO 异常 + */ + public static boolean toOutputStream(InputStream is, OutputStream os, OnWriteCallback onWrote) throws IOException { + return toOutputStream(is, os, onWrote, null); + } + + /** + * 读取输入流并推输出流 + * + * @param is 输入流 + * @param os 输出流 + * @param onWrote 写入回调,参数为已写入数据量,返回 true 时继续传输,否则中断流传输 + * @param beforeClose 流关闭前事件 + * @return true 为正常传输完成,否则发生中断传输 + * @throws IOException IO 异常 + */ + public static boolean toOutputStream(InputStream is, OutputStream os, OnWriteCallback onWrote, Callback beforeClose) throws IOException { + BufferedInputStream bis = new BufferedInputStream(is); + BufferedOutputStream bos = new BufferedOutputStream(os); + byte[] buffer = new byte[4096]; + int l; + + boolean interrupt = false; + if (onWrote == null) { + while ((l = bis.read(buffer)) != -1) { + bos.write(buffer, 0, l); + } + } else { + long c = 0; + while ((l = bis.read(buffer)) != -1) { + if (onWrote.handler((c += l), l)) { + bos.write(buffer, 0, l); + } else { + interrupt = true; + break; + } + } + bos.flush(); + } + if (beforeClose != null) { + beforeClose.handler(); + } + bos.flush(); + bos.close(); + bis.close(); + os.flush(); + os.close(); + is.close(); + return !interrupt; + } + + /** + * 写入字节到文件随机访问对象,此操作不会关闭流 + * + * @param raf 文件随机访问的对象 + * @param bytes 字节 + * @throws IOException IO 异常 + */ + public static void toRandomStream(RandomAccessFile raf, byte[] bytes) throws IOException { + toRandomStream(raf, new ByteArrayInputStream(bytes)); + } + + /** + * 写入数据流到文件随机访问对象,此操作不会关闭流 + * + * @param raf 文件随机访问对象 + * @param is 数据流 + * @throws IOException IO 异常 + */ + public static void toRandomStream(RandomAccessFile raf, InputStream is) throws IOException { + byte[] buffer = new byte[4096]; + int l; + while ((l = is.read(buffer)) != -1) { + raf.write(buffer, 0, l); + } + } + + /** + * 字节输出流转输入流 + * + * @param baos 输出流 + * @return 输入流 + */ + public static ByteArrayInputStream toInputStream(ByteArrayOutputStream baos) { + return new ByteArrayInputStream(baos.toByteArray()); + } + + /** + * 读取文件随机访问对象指定区块为输入流,此操作会读取数据至内存,最大 2GB,不会关闭流 + * + * @param raf 文件随机访问对象 + * @param from 开始字节 + * @param size 读取大小,-1 时将读到结尾 + * @return 读取数据流 + * @throws IOException IO 异常 + */ + public static InputStream toInputStream(RandomAccessFile raf, long from, long size) throws IOException { + raf.seek(from); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + toOutputStream(os, raf, from, size); + return new ByteArrayInputStream(os.toByteArray()); + } + + /** + * 读取文件随机访问对象指定区块到输出流,不会关闭流 + * + * @param os 输出流 + * @param raf 文件随机访问对象 + * @param from 开始字节 + * @param size 读取大小,-1 时将读到结尾 + * @throws IOException IO 异常 + */ + public static void toOutputStream(OutputStream os, RandomAccessFile raf, long from, long size) throws IOException { + byte[] buffer = new byte[4096]; + int l; + long c = 0; + while ((l = raf.read(buffer)) != -1) { + if (0 < size && size - c < 4096) { + os.write(buffer, 0, (int) (size - c)); + break; + } else { + os.write(buffer, 0, l); + c += l; + } + } + } + + /** + * 序列化对象到输出流 + * + * @param os 输出流 + * @param object 可序列化对象 + * @throws IOException IO 异常 + */ + public static void toOutputStream(OutputStream os, Object object) throws IOException { + try (os; ObjectOutputStream oos = new ObjectOutputStream(os)) { + oos.writeObject(object); + } + } + + public static TreeFile treeFile(String path) { + return new TreeFile(path); + } + + /** + * 深度遍历文件夹获取所有文件 + * + * @param file 文件夹 + * @return 所有文件 + */ + public static List listFile(File file) { + List result = new ArrayList<>(); + + if (file != null && file.exists()) { + if (file.isFile()) { + result.add(file); + } else { + // 左出右进,文件夹队列 + Deque deque = new ArrayDeque<>(); + deque.addLast(file); + + while (!deque.isEmpty()) { + File poll = deque.pollFirst(); + + File[] files = poll.listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deque.addLast(files[i]); + } else { + result.add(files[i]); + } + } + } + } + } + } + return result; + } + + public static void copy(File from, String toPath) throws NoPermissionException, IOException { + copy(from, toPath, null); + } + + /** + * 复制文件 + * + * @param from 源文件 + * @param toPath 目标文件夹路径 + * @throws NoPermissionException 无权限 + * @throws IOException IO 异常 + */ + public static void copy(File from, String toPath, OnWriteCallback callback) throws NoPermissionException, IOException { + if (from != null && from.exists()) { + if (from.isFile()) { + File target = file(fitPath(toPath) + from.getName()); + destroy(target); + toOutputStream(getInputStream(from), getOutputStream(target), callback); + } else { + // 左出右进,文件夹队列 + Deque dirctoryDeque = new ArrayDeque<>(); + dirctoryDeque.addLast(from); + + String path; + String basePath = from.getParent(); + while (!dirctoryDeque.isEmpty()) { + File[] files = dirctoryDeque.pollFirst().listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + dirctoryDeque.addLast(files[i]); + } else { + path = files[i].getPath().substring(basePath.length()); + File target = file(fitPath(toPath) + path); + destroy(target); + toOutputStream(getInputStream(files[i]), getOutputStream(target), callback); + } + } + } + } + } + } + } + + /** + * 移动文件 + * + * @param from 源文件 + * @param toPath 目标路径 + * @throws NoPermissionException 无权限 + * @throws IOException IO 异常 + */ + public static void move(File from, String toPath) throws NoPermissionException, IOException { + if (from != null && from.exists()) { + if (from.isFile()) { + Files.move(from.toPath(), file(fitPath(toPath) + from.getName()).toPath(), StandardCopyOption.REPLACE_EXISTING); + } else { + // 左出右进,文件夹队列 + Deque dirctoryDeque = new ArrayDeque<>(); + dirctoryDeque.addLast(from); + + String path; + String basePath = from.getParent(); + while (!dirctoryDeque.isEmpty()) { + File[] files = dirctoryDeque.pollFirst().listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + dirctoryDeque.addLast(files[i]); + } else { + path = files[i].getPath().substring(basePath.length()); + Files.move(files[i].toPath(), file(fitPath(toPath) + path).toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + } + } + } + } + } + + /** + * 删除文件或文件夹,删除失败不响应 + * + * @param file 文件 + */ + public static synchronized void destroy(File file) { + if (file != null && file.exists()) { + // 左出右进,文件夹队列 + Deque dirctoryDeque = new ArrayDeque<>(); + if (file.isDirectory()) { + dirctoryDeque.addLast(file); + } + // 待删文件或文件夹,右进右出,最右边的为最深文件夹 + Deque deleteDeque = new ArrayDeque<>(); + deleteDeque.addLast(file); + + while (!dirctoryDeque.isEmpty()) { + File[] files = dirctoryDeque.pollFirst().listFiles(); + if (files != null) { + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + dirctoryDeque.addLast(files[i]); + deleteDeque.addLast(files[i]); + } else { + if (!files[i].delete()) { + System.err.println(files[i].getAbsolutePath() + " delete fail"); + } + } + } + } + } + while (!deleteDeque.isEmpty()) { + File dir = deleteDeque.pollLast(); + if (!dir.delete()) { + System.err.println(dir.getAbsolutePath() + " delete fail"); + } + } + } + } + + /** + * 读取 jar 内文件为数据流 + * + * @param path jar 内文件路径,不需要 / 开始,如 config/TimiJava.ini + * @return 数据流 + */ + public static InputStream resourceToInputStream(Class clazz, String path) { + return clazz.getClassLoader().getResourceAsStream(path); + } + + /** + * 读取 jar 内文件为数据流 + * + * @param path jar 内文件路径,不需要 / 开始,如 config/TimiJava.ini + * @return 数据流 + */ + public static boolean resourceExist(Class clazz, String path) { + return resourceToInputStream(clazz, path) != null; + } + + /** + * 读取 jar 内文件为字节数据 + * + * @param path jar 内文件路径,不需要 / 开始,如 config/TimiJava.ini + * @return 字节数据 + */ + public static byte[] resourceToBytes(Class clazz, String path) throws IOException { + return toBytes(resourceToInputStream(clazz, path)); + } + + /** + * 读取 jar 内文件为字符串内容(UTF-8) + * + * @param path jar 内文件路径,不需要 / 开始,如 config/TimiJava.ini + * @return 字符串内容 + */ + public static String resourceToString(Class clazz, String path) { + try { + return toString(resourceToInputStream(clazz, path)); + } catch (IOException e) { + e.fillInStackTrace(); + return null; + } + } + + /** + * 复制 jar 内的文件到磁盘(字节流) + * + * @param srcPath jar 内文件路径,不需要 / 开始,如 config/TimiJava.ini + * @param distPath 磁盘路径 + * @return 磁盘文件 + * @throws NoPermissionException 权限异常 + */ + public static File resourceToDisk(Class clazz, String srcPath, String distPath) throws NoPermissionException { + try { + return toFile(distPath, resourceToInputStream(clazz, srcPath)); + } catch (IOException e) { + e.fillInStackTrace(); + return null; + } + } + + /** + * 读取文件所有字节 + * + * @param file 输入流 + * @return 字节数据 + * @throws IOException IO 异常 + */ + public static byte[] toBytes(File file) throws IOException { + return toBytes(getInputStream(file)); + } + + /** + * 读取输入流所有字节 + * + * @param is 输入流 + * @return 字节数据 + */ + public static byte[] toBytes(InputStream is) throws IOException { + byte[] bytes = is.readAllBytes(); + is.close(); + return bytes; + } + + /** + * 字节数组克隆 + * + * @param bytes 输入流 + * @return 字节数据 + */ + public static byte[] bytesClone(byte[] bytes) { + byte[] result = new byte[bytes.length]; + System.arraycopy(bytes, 0, result, 0, bytes.length); + return result; + } + + /** + * 隐藏文件 + * + * @param files 文件列表 + * @throws IOException IO 异常 + */ + public static void hideFile(File... files) throws IOException { + for (int i = 0; i < files.length; i++) { + Files.setAttribute(files[i].toPath(), "dos:hidden", Boolean.TRUE, LinkOption.NOFOLLOW_LINKS); + } + } + + /** + * 计算文件或文件夹大小 + * + * @param file 文件或文件夹 + * @return 总大小(字节) + */ + public static synchronized long calcSize(File file) { + long total = 0; + List files = listFile(file); + for (int i = 0; i < files.size(); i++) { + if (files.get(i).isFile()) { + total += files.get(i).length(); + } + } + return total; + } + + /** + * 计算文件 MD5 + * + * @param file 文件 + * @return MD5 + * @throws NoSuchAlgorithmException JDK 不支持此算法 + */ + public static String md5(File file) throws NoSuchAlgorithmException { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + InputStream is = getInputStream(file); + byte[] buffer = new byte[8192]; + int l; + while ((l = is.read(buffer)) != -1) { + md.update(buffer, 0, l); + } + byte[] bytes = md.digest(); + char[] chars = new char[bytes.length * 2]; + for (int i = 0, j = 0; i < bytes.length; i++) { + chars[j++] = Text.HEX_DIGITS_LOWER[bytes[i] >>> 4 & 0xF]; + chars[j++] = Text.HEX_DIGITS_LOWER[bytes[i] & 0xF]; + } + is.close(); + return new String(chars); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 写入回调 + * + * @author 夜雨 + * @version 2022-11-30 15:08 + */ + public interface OnWriteCallback { + + /** + * 写入回调 + * + * @param total 合计字节 + * @param now 当前缓冲周期字节 + * @return 返回 true 时继续写入,否则中断写入 + */ + default boolean handler(long total, long now) { + return true; + } + } +} diff --git a/src/main/java/com/imyeyu/io/IOSize.java b/src/main/java/com/imyeyu/io/IOSize.java new file mode 100644 index 0000000..6ae4c12 --- /dev/null +++ b/src/main/java/com/imyeyu/io/IOSize.java @@ -0,0 +1,168 @@ +package com.imyeyu.io; + +/** + * 字节大小工具 + * + * @author 夜雨 + * @version 2023-06-01 16:41 + */ +public class IOSize { + + /** + * 储存单位 + * + * @author 夜雨 + * @version 2022-04-08 14:37 + */ + public enum Unit { + + /** B */ + B, + + /** KB */ + KB, + + /** MB */ + MB, + + /** GB */ + GB, + + /** TB */ + TB, + + /** PB */ + PB, + + /** EB */ + EB; + + /** + * 转换指定单位字节量 + * + * @param unit 单位 + * @param value 值 + * @return 该单位值字节量 + */ + public static long value(Unit unit, double value) { + Unit[] values = values(); + for (int i = 0; i < values.length; i++) { + if (values[i] == unit) { + return (long) (value * Math.pow(1024, i)); + } + } + return (long) value; + } + } + + /** 1 字节 */ + public static long BYTE = 1; + + /** 1 KB */ + public static long KB = BYTE << 10; + + /** 1 MB */ + public static long MB = KB << 10; + + /** 1 GB */ + public static long GB = MB << 10; + + /** 1 TB */ + public static long TB = GB << 10; + + /** 1 PB */ + public static long PB = TB << 10; + + /** 1 EB */ + public static long EB = PB << 10; + + + /** + *

格式化一个储存容量,保留两位小数 + *

+	 *     // 返回 100.01 KB
+	 *     Tools.byteFormat(102411, 2);
+	 * 
+ * + * @param size 字节大小 + * @return 格式化结果 + */ + public static String format(double size) { + return format(size, 2, null); + } + + /** + *

格式化一个储存容量 + *

+	 *     // 返回 100.01 KB
+	 *     Tools.byteFormat(102411, 2);
+	 * 
+ * + * @param size 字节大小 + * @param stopUnit 最高等级,格式化到某单位后不再升级,最低 {@link Unit#B},最高 {@link Unit#EB} + * @return 格式化结果 + */ + public static String format(double size, Unit stopUnit) { + return format(size, 2, stopUnit); + } + + /** + *

格式化一个储存容量 + *

+	 *     // 返回 100.01 KB
+	 *     Tools.byteFormat(102411, 2);
+	 * 
+ * + * @param size 字节大小 + * @param decimal 保留小数 + * @param stopUnit 最高等级,格式化到某单位后不再升级,最低 {@link Unit#B},最高 {@link Unit#EB} + * @return 格式化结果 + */ + public static String format(double size, int decimal, Unit stopUnit) { + final Unit[] unit = Unit.values(); + if (0 < size) { + for (int i = 0; i < unit.length; i++, size /= 1024d) { + if (size <= 1000 || i == unit.length - 1 || unit[i] == stopUnit) { + if (i == 0) { + // 最小单位不需要小数 + return (int) size + " B"; + } else { + String format = "%." + decimal + "f " + unit[i]; + return String.format(format, size); + } + } + } + } + return "0 B"; + } + + + /** + *

格式化一个储存容量,不带单位 + *

+	 *     // 返回 100.01
+	 *     Tools.byteFormat(102411, 2);
+	 * 
+ * + * @param size 字节大小 + * @param decimal 保留小数 + * @param stopUnit 最高等级,格式化到某单位后不再升级,最低 {@link Unit#B},最高 {@link Unit#TB} + * @return 格式化结果(不带单位) + */ + public static String formatWithoutUnit(double size, int decimal, Unit stopUnit) { + final Unit[] unit = Unit.values(); + if (0 < size) { + for (int i = 0; i < unit.length; i++, size /= 1024d) { + if (size <= 1000 || i == unit.length - 1 || unit[i] == stopUnit) { + if (i == 0) { + return String.valueOf((int) size); + } else { + String format = "%." + decimal + "f"; + return String.format(format, size); + } + } + } + } + return "0"; + } +} diff --git a/src/main/java/com/imyeyu/io/IOSpeedService.java b/src/main/java/com/imyeyu/io/IOSpeedService.java new file mode 100644 index 0000000..b0815be --- /dev/null +++ b/src/main/java/com/imyeyu/io/IOSpeedService.java @@ -0,0 +1,274 @@ +package com.imyeyu.io; + + +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.CallbackArg; +import com.imyeyu.utils.Calc; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; + +/** + * 字节速度计算,单例服务 + *
+ *     // 开始计算(默认周期 1 秒)
+ *     IOSpeedService.getInstance().start();
+ *
+ *     // 监听全局字节量
+ *     IOSpeedService.getInstance().addBufferListener(d -> System.out.println(d));
+ *
+ *     // 创建字节统计节点
+ *     IOSpeedService.item item = IOSpeedService.getInstance().createItem();
+ *
+ *     // 监听节点字节量
+ *     item.addBufferListener(d -> System.out.println(d));
+ *
+ *     // 推送字节量
+ *     item.push(1024);
+ * 
+ * + * @author 夜雨 + * @version 2021-11-30 12:43 + */ +public class IOSpeedService { + + /** 单例对象 */ + private static IOSpeedService service; + + /** 加盐,用于在数据对齐的 IO 流中看起来像非对齐传输。此操作会使计算数据变得非真实,仅作为 UI 显示时可使用 */ + private long salt; + private Timer timer; + private double totalBufferOld, totalBuffer; + private boolean isRunning; + + private final Map items; + private final List> bufferListeners; + + private IOSpeedService() { + items = new HashMap<>(); + bufferListeners = new ArrayList<>(); + } + + /** 启动服务,默认每秒计算一次 */ + public void start() { + start(1000); + } + + /** + * 启动服务 + * + * @param interval 频率,单位:毫秒 + */ + public void start(int interval) { + if (timer != null) { + shutdown(); + } + timer = new Timer(true); + timer.scheduleAtFixedRate(new TimerTask() { + + @Override + public void run() { + // 单项速度 + Item value; + for (Map.Entry item : items.entrySet()) { + value = item.getValue(); + synchronized (value.bufferListeners) { + long _salt = 0; + if (value.buffer != value.bufferOld) { + _salt = Calc.random(-salt, +salt); + } + for (int i = 0; i < value.bufferListeners.size(); i++) { + value.bufferListeners.get(i).handler(value.buffer - value.bufferOld + _salt); + } + } + value.bufferOld = value.buffer; + } + // 全局速度 + synchronized (bufferListeners) { + long _salt = 0; + if (totalBuffer != totalBufferOld) { + _salt = Calc.random(-salt, +salt); + } + for (int i = 0; i < bufferListeners.size(); i++) { + bufferListeners.get(i).handler(totalBuffer - totalBufferOld + _salt); + } + } + totalBufferOld = totalBuffer; + } + }, 0, interval); + isRunning = true; + } + + /** + * 创建统计节点 + * + * @return 统计节点 + */ + public Item createItem() { + return createItem(null); + } + + /** + * 创建统计节点 + * + * @param customID 自定义 ID + * @return 统计节点 + */ + public Item createItem(String customID) { + Item item = new Item(customID); + item.bufferListeners.add(d -> totalBuffer += d); + items.put(item.id, item); + return item; + } + + /** + * 获取统计节点 + * + * @param id 节点 ID + * @return 统计节点 + */ + public Item item(String id) { + return items.get(id); + } + + /** + * 获取总缓冲量 + * + * @return 缓冲数据量 + */ + public double getTotalBuffer() { + return totalBuffer; + } + + /** 重置计算 */ + public void reset() { + totalBufferOld = totalBuffer = 0; + } + + /** 终止服务 */ + public void shutdown() { + if (timer != null) { + timer.cancel(); + timer.purge(); + timer = null; + } + isRunning = false; + } + + /** + * 获取单例对象 + * + * @return 单例对象 + */ + public static synchronized IOSpeedService getInstance() { + if (service == null) { + service = new IOSpeedService(); + } + return service; + } + + /** + * 是否运行中 + * + * @return true 为运行中 + */ + public boolean isRunning() { + return isRunning; + } + + public long getSalt() { + return salt; + } + + public void setSalt(long salt) { + this.salt = salt; + } + + /** + * 添加全局字节量监听 + * + * @param bufferListener 全局字节量监听 + */ + public void addBufferListener(CallbackArg bufferListener) { + synchronized (bufferListeners) { + bufferListeners.add(bufferListener); + } + } + + /** + * 移除全局字节量监听 + * + * @param bufferListener 全局字节量监听 + */ + public void removeBufferListener(CallbackArg bufferListener) { + synchronized (bufferListeners) { + bufferListeners.remove(bufferListener); + } + } + + /** + * 统计节点,节点之间互相不受影响 + * + * @author 夜雨 + * @version 2023-05-09 10:34 + */ + public static final class Item { + + final List> bufferListeners; + + String id; + double buffer; + double bufferOld; + + Item(String customID) { + if (TimiJava.isEmpty(customID)) { + id = UUID.randomUUID().toString(); + } else { + id = customID; + } + buffer = 0; + bufferListeners = new ArrayList<>(); + } + + /** + * 推送已处理字节量 + * + * @param buffer 字节量 + */ + public void push(double buffer) { + this.buffer += buffer; + } + + /** 重置计算 */ + public void reset() { + bufferOld = buffer = 0; + } + + /** + * 移除字节量监听 + * + * @param bufferListener 全局字节量监听 + */ + public void addBufferListener(CallbackArg bufferListener) { + synchronized (bufferListeners) { + bufferListeners.add(bufferListener); + } + } + + /** + * 移除字节量监听 + * + * @param bufferListener 全局字节量监听 + */ + public void removeBufferListener(CallbackArg bufferListener) { + synchronized (bufferListeners) { + bufferListeners.remove(bufferListener); + } + } + } +} diff --git a/src/main/java/com/imyeyu/io/JarReader.java b/src/main/java/com/imyeyu/io/JarReader.java new file mode 100644 index 0000000..d9ce55f --- /dev/null +++ b/src/main/java/com/imyeyu/io/JarReader.java @@ -0,0 +1,87 @@ +package com.imyeyu.io; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Jar 文件读取 + * + * @author 夜雨 + * @version 2021-12-01 17:39 + */ +public class JarReader extends JarFile { + + private final Map files; + + /** + * 默认构造 + * + * @param file 文件 + * @throws IOException 读取异常 + */ + public JarReader(File file) throws IOException { + super(file); + + files = new HashMap<>(); + + Enumeration entries = entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry != null && !entry.isDirectory()) { + files.put(entry.getName(), getInputStream(entry)); + } + } + } + + /** + * 该文件是否存在 + * + * @param path 路径 + * @return true 为存在 + */ + public boolean has(String path) { + return files.containsKey(path); + } + + /** + * 获取文件输入流 + * + * @param path 路径 + * @return 输入流 + * @throws FileNotFoundException 文件不存在 + */ + public InputStream getInputStream(String path) throws FileNotFoundException { + if (has(path)) { + return files.get(path); + } else { + throw new FileNotFoundException("Not found file: " + path); + } + } + + /** + * 读取某文件为字节数据 + * + * @param path 路径 + * @return 字节数据 + * @throws FileNotFoundException 文件不存在 + */ + public byte[] getBytes(String path) throws IOException { + return IO.toBytes(getInputStream(path)); + } + + /** + * 获取 jar 所有文件的数据流映射列表,Map<路径, 数据流> + * + * @return 数据流映射列表 + */ + public Map getFiles() { + return files; + } +} diff --git a/src/main/java/com/imyeyu/io/TreeFile.java b/src/main/java/com/imyeyu/io/TreeFile.java new file mode 100644 index 0000000..b83aaf6 --- /dev/null +++ b/src/main/java/com/imyeyu/io/TreeFile.java @@ -0,0 +1,74 @@ +package com.imyeyu.io; + +import com.imyeyu.java.TimiJava; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +/** + * @author 夜雨 + * @version 2024-06-30 16:26 + */ +public class TreeFile extends File { + + private List children; + + public TreeFile(String pathname) { + super(pathname); + + if (isDirectory()) { + build(); + } + } + + private void build() { + if (children != null) { + return; + } + File[] files = listFiles(); + if (files == null) { + return; + } + children = new ArrayList<>(); + for (int i = 0; i < files.length; i++) { + TreeFile treeFile = new TreeFile(files[i].getAbsolutePath()); + children.add(treeFile); + if (treeFile.isDirectory()) { + treeFile.build(); + } + } + } + + public void foreach(ForeachCallback callback) throws Exception { + Stack stack = new Stack<>(); + stack.push(this); + + while (!stack.isEmpty()) { + TreeFile item = stack.pop(); + String relPath = item.getAbsolutePath().substring(Math.min(item.getAbsolutePath().length(), getAbsolutePath().length() + 1)); + callback.handler(relPath, item); + if (TimiJava.isNotEmpty(item.children)) { + for (int i = 0, l = item.children.size(); i < l; i++) { + stack.push(item.children.get(i)); + } + } + } + } + + public List getChildren() { + return children; + } + + /** + * + * + * @author 夜雨 + * @since 2024-12-09 23:00 + */ + public interface ForeachCallback { + + void handler(String relPath, File file) throws Exception; + } +} diff --git a/src/test/java/test/TestIO.java b/src/test/java/test/TestIO.java new file mode 100644 index 0000000..724f2de --- /dev/null +++ b/src/test/java/test/TestIO.java @@ -0,0 +1,34 @@ +package test; + +import com.imyeyu.io.IO; +import com.imyeyu.io.IOSize; +import com.imyeyu.utils.Digest; +import com.imyeyu.utils.Time; +import org.junit.jupiter.api.Test; + +import java.io.File; + +/** + * @author 夜雨 + * @since 2024-12-17 22:10 + */ +public class TestIO { + + @Test + public void testCopy() throws Exception { + File from = IO.file("test/test.7z"); + File target = new File("test/dir/test.7z"); + IO.destroy(target); + IO.copy(from, target.getParent(), new IO.OnWriteCallback() { + + @Override + public boolean handler(long total, long now) { + System.out.printf("progress %.2f%n", 1D * total / from.length()); + return true; + } + }); + long startAt = Time.now(); + assert Digest.md5(IO.toString(from)).equals(Digest.md5(IO.toString(target))); + System.out.printf("md5 %sms%n", Time.now() - startAt); + } +} diff --git a/src/test/java/test/TreeFileTest.java b/src/test/java/test/TreeFileTest.java new file mode 100644 index 0000000..ff86794 --- /dev/null +++ b/src/test/java/test/TreeFileTest.java @@ -0,0 +1,23 @@ +package test; + +import com.imyeyu.io.TreeFile; +import org.junit.jupiter.api.Test; + +/** + * @author 夜雨 + * @version 2024-06-30 16:40 + */ +public class TreeFileTest { + + @Test + public void testBuild() { + TreeFile treeFile = new TreeFile("E:\\IDEAProject\\timi-compress"); + System.out.println("123"); + } + + @Test + public void testForeach() throws Exception { + TreeFile treeFile = new TreeFile("E:\\IDEAProject\\timi-io"); + treeFile.foreach(((relPath, file) -> System.out.println(relPath))); + } +}