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)));
+ }
+}