# ShadyDiskManger **Repository Path**: huang_wei-e/shady-disk-manger ## Basic Information - **Project Name**: ShadyDiskManger - **Description**: 学习完Java文件和多线程后使用这两项技术实现的一款磁盘管理系统,包括重复文件查询、删除、备份以及大型文件多线程传输和断点续传 - **Primary Language**: Java - **License**: MulanPSL-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-07-01 - **Last Updated**: 2022-07-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # `shadydiskmanger`项目介绍 ## 1. 项目说明 `shadydiskmanger`是我在学习完`Java`的文件操作后所想要进行开发的一个小型`Java`项目,本项目将结合`md5`算法、Java文件操作和Java多线程的相关知识进行开发,为了符合当前阶段的开发水平在`UI`界面将不使用任何`GUI`组件,单纯使用Java的打印进行显示 ### 1.1 项目功能 本项目是一个完整的磁盘管理系统,其功能都是来自于日常计算机使用过程中所遇到的痛点问题 #### 1.1.1 磁盘重复文件查询、备份和删除 本项目的第一个实现的功能就是能够通过用户输入的目录路径开始进行**层级**遍历,并通过计算文件的`md5`码进行重复判断,若有多个文件的`md5`码相同将视为这些文件为重复文件,经过查询后将向用户展示重复文件的文件名、大小和所在路径用户可以自行选择是直接删除还是先备份后删除或者只导出重复文件的具体信息,需要注意的是必须先将文件备份后才可以对文件进行恢复,所以当用户使用该系统删除重复文件时建议先备份再删除以备不时之需。 #### 1.1.2 文件复制 文件复制是其他功能下的一个子功能,但文件的复制在日常的使用中极为普遍,所以专门将文件复制功能抽取成一个独立的功能,在其他功能中若要进行文件复制直接复用该功能即可。 #### 1.1.3 大型文件传输功能 该功能的实现原理基于Java的多线程,因为对于一个大型文件如果只使用一个线程进行读取和传输那么会十分浪费系统的资源,并且单线程速率慢不利于大型文件的传输,所以将使用多线程技术将一个大型文件分割成多个子文件,再将子文件聚合回原来的文件来达到大型文件传输的功能。 #### 1.1.4 文件断点传输 在文件的传输过程中有可能遇到各种原因导致文件传输过程被迫中断,而如果再次从头对该文件发起传输那么就浪费了之前传输所消耗的资源和时间,所以利用断点传输技术能够将项目从上一次传输中断的地方重新开始传输文件。 ## 2. 版本开发日志 ### 2.1 `v1.0`版本(2020.9.23) #### 2.1.1 版本说明 该版本是最初始的版本,在该版本中完成了对重复文件的查询、备份和删除的功能,但使用的实现技术比较简单,在重复文件查询方面只使用了单线程,所以查询速度慢,效率低。 #### 2.1.2 核心功能开发过程 1. 重复文件的判断 > 在重复文件的判断中采用的是`md5`算法,而在Java中有`MessageDigest`类提供了`md5`算法,能够直接使用该类获得对象的`md5`码再将该`md5`码以`String`的方式返回用于判断 2. 文件目录的遍历 > 在文件的遍历中可以使用深度优先遍历和广度优先遍历的方式,但是深度优先遍历会遍历到文件树的最底层,在一步一步向上遍历不符合正常的文件遍历思维,文件树是树型结构在正常的遍历方式中都是使用层级遍历对树进行遍历的,使用这种遍历方式相对于深度优先遍历前者遍历效率更高,遍历逻辑更加清晰,所以采用`BFS`算法对文件目录进行广度优先遍历 > > ```java > while (!deque.isEmpty()) { > DirectoryNode now = deque.poll(); > File file = new File(now.getPath()); > File[] fileList = file.listFiles(); > for (File i : fileList) { > if (i.isDirectory()) { > deque.add(new DirectoryNode(i.getPath(), now.getRoot() + 1)); > continue; > } > Files files = new Files(i.getName(), i.getPath(), i.length(), FileMd5Utils.md5OfFile(i), i.getParent()); > if (isRepeatFile(files.getMd5())) filesList.add(files); > else md5List.add(files.getMd5()); > } > } > ``` 3. 重复文件的备份和恢复 > 重复文件的备份和恢复所需要使用的主要实现方法就是在Java文件操作中对象的序列化和反序列化,将对象进行序列化能够将对象持久保存在本地,而对象持有本次查询出的所有重复文件的详细信息,将该对象序列化保存在本地,而当需要恢复时只需要读取本地的序列化文件,将其反序列化寻找对应的备份文件夹,并将该文件夹中的文件按照其记录的原位置恢复 > > ```java > int fileCount = 0; > List list; > File file = new File(src + "\\copy.dat"); > if (!file.exists() || file.length() < 0) { > return -1; > } > try { > ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file)); > list = (List) inputStream.readObject(); > inputStream.close(); > } catch (IOException e) { > throw new RuntimeException(e); > } catch (ClassNotFoundException e) { > throw new RuntimeException(e); > } > for (Files files : list) { > Files copyDirectoryFile = new Files(files.getName(), src + '\\' + files.getName(), files.getSize(), files.getMd5(), src); > if (copySignalFile(copyDirectoryFile, files.getParentPath())) fileCount++; > } > return fileCount; > ``` ### 2.2 `v1.0.1`版本(2020.9.24) #### 2.2.1 版本说明 在次日的测试过程中发现项目只能在`Windows`系统下查询重复文件夹,而在`Linux`和其他以`Unix`为内核的操作系统中无法遍历目录,经过检查发现是在`Windows`系统下的目录分隔符与其他系统不同,将目录分隔符修改为`/`后即可遍历`Windows`和其他操作系统 ### 2.3 `v1.1`版本(2020.9.30) #### 2.3.1 版本说明 在学习完多线程后就开始编写大型文件的传输功能,在整个大型文件的传输中需要进行多次的测试来取得线程数和缓存区大小的最佳取值区间测试,并且在进行大型文件的传输过程中对磁盘的读写量较大,多个线程中的缓存数组在没有被`gc`回收前需要占用内存的空间甚至有可能造成内存泄漏的问题,所以需要经过多次的测试来选取合适区间。 #### 2.3.2 最佳线程数测试 想要完成本次测试首先需要完成文件传输的代码,其次要考虑文件分割最后需要对文件是否传输完成进行判断 **文件传输** ```java public void fileCopy() throws IOException { RandomAccessFile readFile = new RandomAccessFile(bigFilel.getSrc(), "r"); RandomAccessFile writeFile = new RandomAccessFile(bigFilel.getDest(), "rws"); readFile.seek(bigFilel.getStartPoint()); writeFile.seek(bigFilel.getStartPoint()); // 缓冲区设置为100MB byte[] buffer = new byte[1024000]; int len = 0; long now = bigFilel.getStartPoint(); //要保证文件传输过程中不会出现越界访问的现象 while ((len = readFile.read(buffer)) != -1 && (now < bigFilel.getStartPoint() + bigFilel.getOperateLength())) { writeFile.write(buffer, 0, len); now = readFile.getFilePointer(); } readFile.close(); writeFile.close(); } ``` **文件分割** ```java public static void createThread() { long fileSize = src.length(); long transferSize = fileSize / THREAD_NUM + 1; // 使用循环来一次性创建多个线程并启动 for (int i = 0; i < THREAD_NUM; i++) { new Thread(new BigFileDao(i * transferSize, transferSize, src, dest)).start(); } } ``` **判断传输是否完成** ```java public static void main(String[] args) { long startTime = System.currentTimeMillis(); createThread(); // 当两文件大小相同时为传输完成 while (src.length() > dest.length()); System.out.println(System.currentTimeMillis() - startTime); System.exit(0); } ``` 传输的文件大小:`2.67 GB (2,877,227,008 字节)` | 线程数 / 个 | 缓冲区大小 / MB | 总耗时 / ms | | :---------: | :-------------: | :---------: | | 20 | 100 | 26729 | | 20 | 128 | 24093 | | 20 | 51.2 | 52462 | | 15 | 100 | 18085 | | 15 | 128 | 32553 | | 15 | 51.2 | 18069 | | 10 | 100 | 17366 | | 10 | 128 | 32276 | | 10 | 51.2 | 20269 | 经过多轮测试考虑传输效率和稳定性最终取用数据为15个线程缓冲区大小为100`MB`,并且发现如果在线程数较少而缓存区较大的情况下会导致缓存区大于该线程所需要传输的数据量最终导致线程会出现阻塞现象。 ### 2.4 `v1.1.1`版本(2020.10.1) #### 2.4.1 版本说明 在整个设计过程中发现代码的耦合度太高,在代码文件结构方面不必要的线程类一种影响了代码的整体可读性,靠考虑到由于`Runnable`是方法接口,所以没必要单独创建一个类带作为线程类,直接使用`lambda`表达式或者方法引用的方式建立线程即可,对于上传大文件的方法是有参数的而`run()`方法是无参的,所以不适合使用方法引用,直接改成`lambda`表达式 **原** ```java new Thread(new BigFileCopyThreadDao(bigFile)).start(); ``` **修改后** ```java FilesFunctionDao filesFunctionDao = new FilesFunctionDao(); Runnable runnable = () -> filesFunctionDao.bigFileCopy(bigFile); new Thread(runnable).start(); ```