这里说的改进是相对于第一版来说的
写这个小工具的目的是方便我在电脑上进行把webdav挂载的云盘中的笔记与本地笔记同步、把平时拍的照片进行多端同步的操作,其实有很多现成的软件可以完美地完成这件事,比如Microsoft SyncToy之类的,不过这是在我做完第一版之后才知道的,所以干脆一不做二不休,再完善一下自己做的小工具,让它更好地胜任我平时需要的文件同步工作。
注意:此工具未经过全面的测试,可能存在一些bug,不建议用在重要文件之上。
对比上个版本,有以下改进
- 支持空文件夹的同步、无效文件夹的删除
- 另起线程进行费时的查找、同步操作,避免堵塞swing线程
- 同步操作有可视化的进度百分比,当同步的文件数量很多或者同步的目标文件夹是在本地挂载的云盘中,导致上传文件受网速限制时,这个进度百分比还是挺有用的。
目前处于学习阶段,代码改得比较乱,可能存在很多不必要的操作影响性能,欢迎大家指出不足之处。
源码
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class File_Syn
{
static Map<String, File> fileMap = new HashMap<>();
static Map<String, File> folderMap = new HashMap<>();
static Map<String, File> fileMap1 = new HashMap<>();
static Map<String, File> fileMap2 = new HashMap<>();
static Map<String, File> folderMap1 = new HashMap<>();
static Map<String, File> folderMap2 = new HashMap<>();
static Map<String, File> deleteFileMap = new HashMap<>();
static Map<String, File> nullFolder = new HashMap<>();
static int count = 0;
public static void main(String[] args) { new Form();}
/**
*
* @param file 源文件夹
* @param fileMap 文件夹中的文件Map
* @param sourceFilePath 源文件夹路径
* @return Map
*/
public static Map<String, File> getFileMap(File file, Map<String, File> fileMap, Map<String, File> folderMap,String sourceFilePath)
{
for (int i = 0; i < file.listFiles().length; i++)
{
File childFile = file.listFiles()[i];
if (!childFile.isDirectory()) //如果不是文件夹
{
String childPath = childFile.getAbsolutePath(); //获取绝对路径
String relativePath = childPath.substring(sourceFilePath.length());//从绝对路径中取得相对路径
fileMap.put(relativePath,childFile);
}
else
{
folderMap.put(childFile.getAbsolutePath().substring(sourceFilePath.length()),childFile); //获取文件夹结构
getFileMap(childFile, fileMap, folderMap,sourceFilePath);//递归查询
}
}
return fileMap;
}
/**
* 查询两个文件夹中的差异文件个数,将差异文件路径存储在fileMap中
* @param sourceFile 源文件夹
* @param destinationFile 目标文件夹
*/
public static void search(File sourceFile, File destinationFile)
{
fileMap1 = getFileMap(sourceFile, fileMap1, folderMap1,sourceFile.getAbsolutePath());
fileMap2 = getFileMap(destinationFile, fileMap2, folderMap2,destinationFile.getAbsolutePath());
for (Map.Entry<String, File> entry : fileMap1.entrySet()) //遍历源文件夹的文件
{
String key = entry.getKey();
if (fileMap2.containsKey(key)) //如果目标文件夹中存在此文件
{
Date date1 = new Date(entry.getValue().lastModified());
Date date2 = new Date(fileMap2.get(key).lastModified());
if (date1.after(date2)) //对比最后修改时间,如果源文件夹中文件较新,则更新
{
fileMap.put(key, entry.getValue());
}
}
else
{
fileMap.put(key, entry.getValue());
}
}
for (Map.Entry<String, File> entry : folderMap1.entrySet()) //遍历文件夹
{
String key = entry.getKey();
if (!folderMap2.containsKey(key))
{
folderMap.put(key,entry.getValue());
}
}
for (Map.Entry<String, File> entry : fileMap2.entrySet()) //需要删除的差异
{
String key = entry.getKey();
if (!fileMap1.containsKey(key))
{
deleteFileMap.put(key,entry.getValue());
}
}
for (Map.Entry<String, File> entry : folderMap2.entrySet()) //无效文件夹
{
String key = entry.getKey();
if (!folderMap1.containsKey(key))
{
nullFolder.put(key,entry.getValue());
}
}
}
/**
*
* @param destinationFile 目标文件夹
* @param jTextArea 文本域
*/
public static void change(File destinationFile,JTextArea jTextArea)
{
String destinationFilePath = destinationFile.getAbsolutePath();
for (Map.Entry<String, File> entry : folderMap.entrySet()) //建立文件夹结构
{
String key = entry.getKey();
File file = new File(destinationFilePath + key);
file.mkdirs();
}
for (Map.Entry<String, File> entry : fileMap.entrySet()) //同步文件
{
String key = entry.getKey();
File file1 = entry.getValue();
File file2 = new File(destinationFilePath + key);
if (fileMap2.containsKey(key))
{
file2 = fileMap2.get(key);
file2.delete(); //删除已存在的文件
}
try {
Files.copy(file1.toPath(), file2.toPath()); //文件复制
count++;
}catch (IOException e) {
e.printStackTrace();
jTextArea.setText("同步失败");
}
}
for (Map.Entry<String, File> entry : deleteFileMap.entrySet()) //删除无效文件
{
File deleteFile = entry.getValue();
if (deleteFile.exists())
{
deleteFile.delete();
}
count++;
}
for (Map.Entry<String, File> entry : nullFolder.entrySet()) //删除无效文件夹
{
File deleteFolder = entry.getValue();
if (deleteFolder.exists())
deleteFolder(deleteFolder);
}
jTextArea.setText("同步完成");
}
public static void deleteFolder(File file)
{
File[] files = file.listFiles();
if (files.length != 0)
{
for (int i = 0; i < files.length; i++)
deleteFolder(files[i]);
}
file.delete();
count++;
}
public static void clear()
{
folderMap.clear();fileMap.clear();
fileMap1.clear();fileMap2.clear();
folderMap1.clear();folderMap2.clear();
deleteFileMap.clear();nullFolder.clear();
count = 0;
}
}
class Form extends JFrame
{
Font font = new Font("宋体",0,20);
JButton sourceBtn = new JButton("选择源文件夹");
JButton destinationBtn = new JButton("选择目标文件夹");
JButton compare = new JButton("对比文件夹");
JButton change = new JButton("同步");
JTextArea jTextArea1 = new JTextArea(2,25);
JTextArea jTextArea2 = new JTextArea(2,25);
JTextArea result = new JTextArea(3,25);
JPanel jPanel1 = new JPanel();
JPanel jPanel2 = new JPanel();
JPanel jPanel3 = new JPanel();
JPanel jPanel4 = new JPanel();
JPanel jPanel5 = new JPanel();
JFileChooser jFileChooser = new JFileChooser();
File sourceFilePath = null;
File destinationFilePath = null;
Form()
{
setSize(300,450);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new GridLayout(5,1));
setLocationRelativeTo(null);
setVisible(true);
jTextArea1.setFont(font);jTextArea1.setLineWrap(true);jTextArea1.setWrapStyleWord(true);
jTextArea2.setFont(font);jTextArea2.setLineWrap(true);jTextArea2.setWrapStyleWord(true);
result.setFont(font);result.setLineWrap(true);result.setWrapStyleWord(true);
jPanel1.add(sourceBtn);jPanel1.add(destinationBtn);
jPanel2.add(jTextArea1);
jPanel3.add(jTextArea2);
jPanel4.add(compare);jPanel4.add(change);
jPanel5.add(result);
add(jPanel1);add(jPanel2);add(jPanel3);add(jPanel4);add(jPanel5);
jFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
sourceBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int returnVal = jFileChooser.showOpenDialog(null);
if (returnVal == JFileChooser.APPROVE_OPTION)
{
sourceFilePath = jFileChooser.getSelectedFile();
jTextArea1.setText(sourceFilePath.getAbsolutePath());
}
}
});
destinationBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int returnVal = jFileChooser.showOpenDialog(null);
if (returnVal == JFileChooser.APPROVE_OPTION)
{
destinationFilePath = jFileChooser.getSelectedFile();
jTextArea2.setText(destinationFilePath.getAbsolutePath());
}
}
});
compare.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (sourceFilePath == null || destinationFilePath == null)
{
result.setText("未选择文件夹");
return;
}
if (sourceFilePath.equals(destinationFilePath))
{
result.setText("源文件夹和目标文件夹不能相同");
return;
}
File_Syn.clear();
result.setText("正在查找差异文件");
new Thread(new t1()).start();
}
});
change.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (File_Syn.fileMap.isEmpty() && File_Syn.deleteFileMap.isEmpty() && File_Syn.nullFolder.isEmpty())
{
result.setText("文件差异列表为空");
return;
}
new Thread(new t2_per()).start();
new Thread(new t2()).start();
}
});
}
class t1 implements Runnable
{
@Override
public void run() {
long start = System.currentTimeMillis();
File_Syn.search(sourceFilePath,destinationFilePath);
long finish = System.currentTimeMillis();
result.setText("查找到"+(File_Syn.fileMap.size()+File_Syn.deleteFileMap.size()+File_Syn.nullFolder.size())+"处差异,其中"+File_Syn.nullFolder.size()+"个无效文件夹");
if ((finish - start) > 1000)
result.append("\n执行耗时:"+(finish-start)/1000+"秒");
else
result.append("\n执行耗时:"+(finish-start)+"毫秒");
}
}
class t2 implements Runnable
{
@Override
public void run() {
long start = System.currentTimeMillis();
File_Syn.change(destinationFilePath,result);
long finish = System.currentTimeMillis();
if ((finish - start) > 1000)
result.append("\n执行耗时:"+(finish-start)/1000+"秒");
else
result.append("\n执行耗时:"+(finish-start)+"毫秒");
}
}
class t2_per implements Runnable
{
@Override
public void run() {
BigDecimal percentage;
int all = File_Syn.fileMap.size()+File_Syn.deleteFileMap.size()+File_Syn.nullFolder.size();
BigDecimal bigAll = new BigDecimal(all);
BigDecimal b100 = new BigDecimal(1);
DecimalFormat decimalFormat = new DecimalFormat("0.00%");
do {
percentage = new BigDecimal(File_Syn.count).divide(bigAll,4, BigDecimal.ROUND_HALF_DOWN);
if (percentage.compareTo(b100) == -1)
result.setText("正在处理,进度:"+decimalFormat.format(percentage));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (percentage.compareTo(b100) == -1);
if (bigAll.compareTo(new BigDecimal(File_Syn.count)) == 0)
File_Syn.clear();
else
result.setText("同步存在异常,未完全完成");
}
}
}
运行截图