├── FileSync
    ├── .classpath
    ├── .project
    ├── .settings
    │   ├── org.eclipse.core.resources.prefs
    │   └── org.eclipse.jdt.core.prefs
    ├── docs
    │   └── note.txt
    └── src
    │   └── com
    │       └── hjh
    │           └── file
    │               └── sync
    │                   ├── core
    │                       ├── FSConfig.java
    │                       ├── KeyGeneral.java
    │                       ├── SyncFolderInfo.java
    │                       └── SyncItem.java
    │                   ├── main
    │                       └── Main.java
    │                   ├── process
    │                       ├── CancelControl.java
    │                       ├── IProcessListener.java
    │                       ├── ProcessPrinter.java
    │                       └── SimpleProcessListener.java
    │                   └── util
    │                       ├── FileUtils.java
    │                       ├── LogHelper.java
    │                       └── MD5.java
├── README.md
├── fileSync.jar
└── run.png
/FileSync/.classpath:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 	
4 | 	
5 | 	
6 | 
7 | 
--------------------------------------------------------------------------------
/FileSync/.project:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 	FileSync
 4 | 	
 5 | 	
 6 | 	
 7 | 	
 8 | 		
 9 | 			org.eclipse.jdt.core.javabuilder
10 | 			
11 | 			
12 | 		
13 | 	
14 | 	
15 | 		org.eclipse.jdt.core.javanature
16 | 	
17 | 
18 | 
--------------------------------------------------------------------------------
/FileSync/.settings/org.eclipse.core.resources.prefs:
--------------------------------------------------------------------------------
1 | eclipse.preferences.version=1
2 | encoding/=UTF-8
3 | 
--------------------------------------------------------------------------------
/FileSync/.settings/org.eclipse.jdt.core.prefs:
--------------------------------------------------------------------------------
 1 | eclipse.preferences.version=1
 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
 5 | org.eclipse.jdt.core.compiler.compliance=1.7
 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate
 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate
 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
11 | org.eclipse.jdt.core.compiler.source=1.7
12 | 
--------------------------------------------------------------------------------
/FileSync/docs/note.txt:
--------------------------------------------------------------------------------
1 | The main class is 'com.hjh.file.sync.main.Main'
2 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/core/FSConfig.java:
--------------------------------------------------------------------------------
 1 | package com.hjh.file.sync.core;
 2 | 
 3 | import java.io.File;
 4 | import java.io.FileInputStream;
 5 | import java.io.FileOutputStream;
 6 | import java.io.IOException;
 7 | import java.io.UnsupportedEncodingException;
 8 | import java.util.UUID;
 9 | 
10 | import com.hjh.file.sync.util.LogHelper;
11 | 
12 | /**
13 |  * 
14 |  * 属性配置
15 |  * 
16 |  * @author 洪 qq:2260806429
17 |  * 
18 |  */
19 | public class FSConfig {
20 | 
21 | 	public static final String TEST_PATH = "E:\\testsync_source";
22 | 
23 | 	public static final String CACHE_ID_FILE = ".hsync";
24 | 
25 | 	public static final long BLOCK_SIZE = 1024 * 1024 * 10; // 10M的块大小
26 | 
27 | 	public final static String INFO_SEP = ":";
28 | 	public final static String CONTENT_SEP = ";";
29 | 
30 | 	public final static int FOLDER_INFO_LEN = 2;
31 | 	public final static int NAME_INDEX = 0;
32 | 	public final static int TIME_INDEX = 1;
33 | 	public final static int SIZE_INDEX = 2;
34 | 	public final static int CONTENT_INDEX = 3;
35 | 
36 | 	public static boolean isCompareBinary() { // 是否二进制比较,否则时间比较
37 | 		return false;
38 | 	}
39 | 
40 | 	public static boolean isIgnore(File file) { // 文件是否忽略
41 | 		if (file.getName().equals(CACHE_ID_FILE)) {
42 | 			return true;
43 | 		}
44 | 		return false;
45 | 	}
46 | 
47 | 	private static File cacheFile(String path, File target) {
48 | 		try {
49 | 			return new File(new File(target, CACHE_ID_FILE), UUID
50 | 					.nameUUIDFromBytes(path.getBytes("utf-8")).toString());
51 | 		} catch (UnsupportedEncodingException e) {
52 | 			LogHelper.error(e);
53 | 		}
54 | 		throw new RuntimeException("生成缓存路径失败:" + path);
55 | 	}
56 | 
57 | 	public static String getCache(String path, File target) throws IOException {
58 | 		File cacheFile = cacheFile(path, target);
59 | 		if (cacheFile.isFile()) {
60 | 			File source = new File(target.getAbsolutePath() + path);
61 | 			if (source.lastModified() == cacheFile.lastModified()) {
62 | 				FileInputStream in = new FileInputStream(cacheFile);
63 | 				byte[] cache = new byte[(int) cacheFile.length()];
64 | 				try {
65 | 					in.read(cache);
66 | 				} finally {
67 | 					in.close();
68 | 				}
69 | 				return new String(cache);
70 | 			} else {
71 | 				LogHelper.warn("缓存KEY文件过期:" + path);
72 | 			}
73 | 		}
74 | 		return null;
75 | 	}
76 | 
77 | 	public static void cache(String id, File target) throws IOException {
78 | 		String[] arr = id.split(INFO_SEP);
79 | 		long size = Long.parseLong(arr[SIZE_INDEX]);
80 | 		if (size > BLOCK_SIZE * 10) {
81 | 			String path = arr[NAME_INDEX];
82 | 			String content = arr[CONTENT_INDEX];
83 | 			File save = cacheFile(path, target);
84 | 			if (!save.getParentFile().exists()) {
85 | 				save.getParentFile().mkdir();
86 | 			}
87 | 			FileOutputStream out = new FileOutputStream(save);
88 | 			try {
89 | 				out.write(content.getBytes());
90 | 			} finally {
91 | 				out.close();
92 | 			}
93 | 			save.setLastModified(new File(target.getAbsolutePath() + path)
94 | 					.lastModified());
95 | 		}
96 | 	}
97 | 
98 | }
99 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/core/KeyGeneral.java:
--------------------------------------------------------------------------------
  1 | package com.hjh.file.sync.core;
  2 | 
  3 | import java.io.File;
  4 | import java.io.FileInputStream;
  5 | import java.io.IOException;
  6 | 
  7 | import com.hjh.file.sync.process.IProcessListener;
  8 | import com.hjh.file.sync.process.SimpleProcessListener;
  9 | import com.hjh.file.sync.util.LogHelper;
 10 | import com.hjh.file.sync.util.MD5;
 11 | 
 12 | /**
 13 |  * 文件指纹计算
 14 |  * 
 15 |  * @author 洪 qq:2260806429
 16 |  */
 17 | public class KeyGeneral {
 18 | 
 19 | 	public static void main(String argv[]) throws IOException {
 20 | 		final String path = "G:\\disk";
 21 | 		// FSConfig.TEST_PATH;
 22 | 		for (File item : new File(path).listFiles()) {
 23 | 			if (item.isFile()) {
 24 | 				genId(item.getAbsolutePath(), new File(path));
 25 | 			}
 26 | 		}
 27 | 	}
 28 | 
 29 | 	private static void genId(String path, File root) throws IOException {
 30 | 		final IProcessListener listener = new SimpleProcessListener(null,
 31 | 				new File(path).isDirectory() ? 0 : new File(path).length());
 32 | 		listener.print("gen id " + new File(path).getName());
 33 | 		String id = id(path, listener, root, true);
 34 | 		LogHelper.info("ID:" + id);
 35 | 	}
 36 | 
 37 | 	public static String id(String filePath, IProcessListener listener,
 38 | 			File root, boolean fromcache) throws IOException {
 39 | 		File file = new File(filePath);
 40 | 		StringBuffer idBuf = new StringBuffer();
 41 | 		try {
 42 | 			idBuf.append(file.getCanonicalPath());
 43 | 		} catch (IOException e) {
 44 | 			throw new RuntimeException("获取路径失败:" + filePath, e);
 45 | 		}
 46 | 		idBuf.append(FSConfig.INFO_SEP);
 47 | 		idBuf.append(file.lastModified());
 48 | 		boolean tocache = false;
 49 | 		if (file.isFile()) {
 50 | 			idBuf.append(FSConfig.INFO_SEP);
 51 | 			idBuf.append(file.length());
 52 | 			idBuf.append(FSConfig.INFO_SEP);
 53 | 			String key = null;
 54 | 			if (fromcache) {
 55 | 				key = FSConfig.getCache(
 56 | 						filePath.substring(root.getAbsolutePath().length()),
 57 | 						root);
 58 | 				if (null != key) {
 59 | 					listener.work(file.length());
 60 | 				}
 61 | 			}
 62 | 			if (null == key) {
 63 | 				key = key(filePath, listener);
 64 | 				tocache = null != key;
 65 | 			}
 66 | 			if (null == key) {
 67 | 				return null;
 68 | 			}
 69 | 			idBuf.append(key);
 70 | 		}
 71 | 		String id = idBuf.toString().substring(root.getAbsolutePath().length());
 72 | 		if (tocache) {
 73 | 			FSConfig.cache(id, root);
 74 | 		}
 75 | 		return id;
 76 | 	}
 77 | 
 78 | 	public static String key(String filePath, IProcessListener listener)
 79 | 			throws IOException {
 80 | 
 81 | 		StringBuffer keyBuf = new StringBuffer();
 82 | 		File file = new File(filePath);
 83 | 		FileInputStream in = new FileInputStream(file);
 84 | 		byte[] cache = new byte[(int) FSConfig.BLOCK_SIZE];
 85 | 		try {
 86 | 			int len = 0;
 87 | 			while (true) {
 88 | 				if (listener.isCancel()) {
 89 | 					return null;
 90 | 				}
 91 | 				len = in.read(cache);
 92 | 				if (len <= 0) {
 93 | 					break;
 94 | 				}
 95 | 				keyBuf.append(FSConfig.CONTENT_SEP);
 96 | 				keyBuf.append(MD5.md5(cache, 0, len));
 97 | 				listener.work(len);
 98 | 			}
 99 | 		} finally {
100 | 			in.close();
101 | 		}
102 | 
103 | 		byte[] allKeys = keyBuf.toString().getBytes();
104 | 		String sumkey = MD5.md5(allKeys, 0, allKeys.length);
105 | 
106 | 		return sumkey + keyBuf.toString();
107 | 	}
108 | 
109 | }
110 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/core/SyncFolderInfo.java:
--------------------------------------------------------------------------------
  1 | package com.hjh.file.sync.core;
  2 | 
  3 | import java.io.File;
  4 | import java.io.IOException;
  5 | import java.util.ArrayList;
  6 | import java.util.Collections;
  7 | import java.util.List;
  8 | 
  9 | import com.hjh.file.sync.process.IProcessListener;
 10 | import com.hjh.file.sync.process.SimpleProcessListener;
 11 | 
 12 | /**
 13 |  * 目录信息
 14 |  * 
 15 |  * @author 洪 qq:2260806429
 16 |  */
 17 | public class SyncFolderInfo {
 18 | 
 19 | 	public static void main(String argv[]) throws IOException {
 20 | 		final String path = FSConfig.TEST_PATH;
 21 | 		final IProcessListener listener = new SimpleProcessListener();
 22 | 		listener.print("test scan");
 23 | 		System.out.println(new SyncFolderInfo(new File(path)).scan(listener)
 24 | 				.printInfo());
 25 | 	}
 26 | 
 27 | 	private File folder;
 28 | 	private long folderCount;
 29 | 	private long fileCount;
 30 | 	private long dataSize;
 31 | 	private List fileKeys;
 32 | 
 33 | 	public SyncFolderInfo(File folder) {
 34 | 		this.folder = folder;
 35 | 	}
 36 | 
 37 | 	private void reset() {
 38 | 		folderCount = 0;
 39 | 		fileCount = 0;
 40 | 		dataSize = 0;
 41 | 		fileKeys = new ArrayList();
 42 | 	}
 43 | 
 44 | 	public String printInfo() {
 45 | 		StringBuffer buf = new StringBuffer();
 46 | 		buf.append(String.format("folder %d file %d dataSize %d \r\n",
 47 | 				folderCount, fileCount, dataSize));
 48 | 		for (String item : fileKeys) {
 49 | 			buf.append(item);
 50 | 			buf.append("\r\n");
 51 | 		}
 52 | 		return buf.toString();
 53 | 	}
 54 | 
 55 | 	public List sync(SyncFolderInfo target) {
 56 | 		List ret = new ArrayList();
 57 | 		int maxSource = this.fileKeys.size();
 58 | 		int maxTarget = target.fileKeys.size();
 59 | 		for (int i = 0, j = 0; i < maxSource || j < maxTarget;) {
 60 | 			String keysource = i >= maxSource ? null : this.fileKeys.get(i);
 61 | 			String keytarget = j >= maxTarget ? null : target.fileKeys.get(j);
 62 | 			if (keysource == null) {
 63 | 				ret.add(new SyncItem(null, keytarget));
 64 | 				j++;
 65 | 			} else if (keytarget == null) {
 66 | 				ret.add(new SyncItem(keysource, null));
 67 | 				i++;
 68 | 			} else {
 69 | 				String pathsource = keysource.split(FSConfig.INFO_SEP)[FSConfig.NAME_INDEX];
 70 | 				String pathtarget = keytarget.split(FSConfig.INFO_SEP)[FSConfig.NAME_INDEX];
 71 | 				int cmp = pathsource.compareTo(pathtarget);
 72 | 				if (0 == cmp) {
 73 | 					if (!keysource.equals(keytarget)) {
 74 | 						ret.add(new SyncItem(keysource, keytarget));
 75 | 					}
 76 | 					i++;
 77 | 					j++;
 78 | 				} else if (cmp > 0) {
 79 | 					ret.add(new SyncItem(null, keytarget));
 80 | 					j++;
 81 | 				} else {
 82 | 					ret.add(new SyncItem(keysource, null));
 83 | 					i++;
 84 | 				}
 85 | 			}
 86 | 		}
 87 | 		return ret;
 88 | 	}
 89 | 
 90 | 	public SyncFolderInfo scan(IProcessListener listener) throws IOException {
 91 | 		reset();
 92 | 		if (!this.folder.exists()) {
 93 | 			listener.updateTotalSize(0);
 94 | 			return this;
 95 | 		}
 96 | 		count(folder, listener);
 97 | 		if (listener.isCancel()) {
 98 | 			return this;
 99 | 		}
100 | 		listener.updateTotalSize(dataSize);
101 | 		key(folder, listener);
102 | 		Collections.sort(fileKeys);
103 | 		return this;
104 | 	}
105 | 
106 | 	private void key(File file, IProcessListener listener) throws IOException {
107 | 		if (FSConfig.isIgnore(file)) {
108 | 			return;
109 | 		}
110 | 		String id = KeyGeneral.id(file.getAbsolutePath(), listener, folder,
111 | 				true);
112 | 		if (id.length() > 0) {
113 | 			fileKeys.add(id);
114 | 		}
115 | 		if (file.isDirectory()) {
116 | 			if (listener.isCancel()) {
117 | 				return;
118 | 			}
119 | 			File[] list = file.listFiles();
120 | 			if (null != list) {
121 | 				for (File item : list) {
122 | 					key(item, listener);
123 | 					if (listener.isCancel()) {
124 | 						return;
125 | 					}
126 | 				}
127 | 			}
128 | 		}
129 | 	}
130 | 
131 | 	private void count(File file, IProcessListener listener) {
132 | 		if (FSConfig.isIgnore(file)) {
133 | 			return;
134 | 		}
135 | 		if (file.isDirectory()) {
136 | 			folderCount++;
137 | 			if (listener.isCancel()) {
138 | 				return;
139 | 			}
140 | 			File[] list = file.listFiles();
141 | 			if (null != list) {
142 | 				for (File item : list) {
143 | 					count(item, listener);
144 | 					if (listener.isCancel()) {
145 | 						return;
146 | 					}
147 | 				}
148 | 			}
149 | 		} else {
150 | 			fileCount++;
151 | 			dataSize += file.length();
152 | 		}
153 | 	}
154 | 
155 | }
156 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/core/SyncItem.java:
--------------------------------------------------------------------------------
  1 | package com.hjh.file.sync.core;
  2 | 
  3 | import java.io.File;
  4 | import java.io.FileInputStream;
  5 | import java.io.FileOutputStream;
  6 | import java.io.IOException;
  7 | import java.io.RandomAccessFile;
  8 | 
  9 | import com.hjh.file.sync.process.IProcessListener;
 10 | import com.hjh.file.sync.util.LogHelper;
 11 | 
 12 | /**
 13 |  * 同步项
 14 |  * 
 15 |  * @author 洪 qq:2260806429
 16 |  */
 17 | public class SyncItem {
 18 | 
 19 | 	private File targetFile;
 20 | 	public String from;
 21 | 	public String to;
 22 | 
 23 | 	public SyncItem(String from, String to) {
 24 | 		this.from = from;
 25 | 		this.to = to;
 26 | 	}
 27 | 
 28 | 	public long getSyncSize() {
 29 | 		if (isDel()) {
 30 | 			return 0;
 31 | 		}
 32 | 		String[] fromArr = from.split(FSConfig.INFO_SEP);
 33 | 		long fromSize = fromArr.length == FSConfig.FOLDER_INFO_LEN ? 0 : Long
 34 | 				.parseLong(fromArr[FSConfig.SIZE_INDEX]);
 35 | 		if (isCopy() || 0 == fromSize) {
 36 | 			return fromSize;
 37 | 		}
 38 | 		long size = 0;
 39 | 		String[] fromContent = fromArr[FSConfig.CONTENT_INDEX]
 40 | 				.split(FSConfig.CONTENT_SEP);
 41 | 		String[] toContent = to.split(FSConfig.INFO_SEP)[FSConfig.CONTENT_INDEX]
 42 | 				.split(FSConfig.CONTENT_SEP);
 43 | 		for (int i = 1; i < fromContent.length; i++) {
 44 | 			if (i < toContent.length) {
 45 | 				if (toContent[i].equals(fromContent[i])) {
 46 | 					continue;
 47 | 				}
 48 | 			}
 49 | 			if (i == fromContent.length - 1) {
 50 | 				size += (fromSize - FSConfig.BLOCK_SIZE * (i - 1));
 51 | 			} else {
 52 | 				size += FSConfig.BLOCK_SIZE;
 53 | 			}
 54 | 		}
 55 | 		return size;
 56 | 	}
 57 | 
 58 | 	public String toString() {
 59 | 		if (isDel()) {
 60 | 			return "del:" + to;
 61 | 		}
 62 | 		if (isCopy()) {
 63 | 			return "add:" + from;
 64 | 		}
 65 | 		return "update:" + from + ":" + to;
 66 | 	}
 67 | 
 68 | 	public boolean isDel() {
 69 | 		return null == from;
 70 | 	}
 71 | 
 72 | 	public boolean isCopy() {
 73 | 		return null == to;
 74 | 	}
 75 | 
 76 | 	public boolean isUpdate() {
 77 | 		return !isDel() && !isCopy();
 78 | 	}
 79 | 
 80 | 	public void sync(File sourceFile, File targetFile,
 81 | 			IProcessListener listener_sync) throws IOException {
 82 | 		this.targetFile = targetFile;
 83 | 		if (listener_sync.isCancel()) {
 84 | 			return;
 85 | 		}
 86 | 		if (isDel()) {
 87 | 			del(toPath(targetFile), listener_sync);
 88 | 		} else if (isCopy()) {
 89 | 			copy(fromPath(sourceFile), toPath(targetFile), listener_sync);
 90 | 		} else {
 91 | 			update(fromPath(sourceFile), toPath(targetFile), listener_sync);
 92 | 		}
 93 | 	}
 94 | 
 95 | 	private String fromPath(File sourceFile) {
 96 | 		return sourceFile.getAbsolutePath()
 97 | 				+ from.split(FSConfig.INFO_SEP)[FSConfig.NAME_INDEX];
 98 | 	}
 99 | 
100 | 	private String toPath(File targetFile) {
101 | 		return targetFile.getAbsolutePath()
102 | 				+ (to == null ? from : to).split(FSConfig.INFO_SEP)[FSConfig.NAME_INDEX];
103 | 	}
104 | 
105 | 	private void del(String path, IProcessListener listener_sync) {
106 | 		File file = new File(path);
107 | 		if (!file.exists()) {
108 | 			return;
109 | 		}
110 | 		if (file.isDirectory()) {
111 | 			File list[] = file.listFiles();
112 | 			if (null != list) {
113 | 				for (File item : list) {
114 | 					if (listener_sync.isCancel()) {
115 | 						return;
116 | 					}
117 | 					del(item.getAbsolutePath(), listener_sync);
118 | 				}
119 | 			}
120 | 		}
121 | 		if (listener_sync.isCancel()) {
122 | 			return;
123 | 		}
124 | 		if (!file.delete()) {
125 | 			LogHelper.warn("删除文件失败:" + file.getAbsolutePath());
126 | 		}
127 | 	}
128 | 
129 | 	private void copy(String fromPath, String toPath,
130 | 			IProcessListener listener_sync) throws IOException {
131 | 		if (listener_sync.isCancel()) {
132 | 			return;
133 | 		}
134 | 		File from = new File(fromPath);
135 | 		File to = new File(toPath);
136 | 		if (!from.exists()) {
137 | 			LogHelper.warn("文件不存在:" + from.getAbsolutePath());
138 | 			return;
139 | 		}
140 | 		if (from.isDirectory()) {
141 | 			if (to.isFile()) {
142 | 				to.delete();
143 | 			}
144 | 			if (!to.isDirectory()) {
145 | 				to.mkdir();
146 | 			}
147 | 			to.setLastModified(from.lastModified());
148 | 		} else {
149 | 			if (to.isDirectory()) {
150 | 				del(toPath, listener_sync);
151 | 			}
152 | 			FileInputStream in = new FileInputStream(from);
153 | 			FileOutputStream out = new FileOutputStream(to);
154 | 			byte[] cache = new byte[(int) FSConfig.BLOCK_SIZE];
155 | 			try {
156 | 				int len = 0;
157 | 				while (true) {
158 | 					if (listener_sync.isCancel()) {
159 | 						return;
160 | 					}
161 | 					len = in.read(cache);
162 | 					if (len <= 0) {
163 | 						break;
164 | 					}
165 | 					out.write(cache, 0, len);
166 | 					listener_sync.work(len);
167 | 				}
168 | 				out.close();
169 | 				out = null;
170 | 				to.setLastModified(from.lastModified());
171 | 				FSConfig.cache(this.from, targetFile);
172 | 			} finally {
173 | 				in.close();
174 | 				if (null != out) {
175 | 					out.close();
176 | 				}
177 | 			}
178 | 		}
179 | 	}
180 | 
181 | 	private void update(String fromPath, String toPath,
182 | 			IProcessListener listener_sync) throws IOException {
183 | 		if (listener_sync.isCancel()) {
184 | 			return;
185 | 		}
186 | 		File from = new File(fromPath);
187 | 		File to = new File(toPath);
188 | 		if (!from.exists()) {
189 | 			LogHelper.warn("文件不存在:" + from.getAbsolutePath());
190 | 			return;
191 | 		}
192 | 		if (from.isDirectory()) {
193 | 			if (to.isFile()) {
194 | 				to.delete();
195 | 			}
196 | 			if (!to.isDirectory()) {
197 | 				to.mkdir();
198 | 			}
199 | 			to.setLastModified(from.lastModified());
200 | 		} else {
201 | 			if (to.isDirectory()) {
202 | 				del(toPath, listener_sync);
203 | 			}
204 | 			RandomAccessFile in = new RandomAccessFile(from, "r");
205 | 			RandomAccessFile out = new RandomAccessFile(to, "rw");
206 | 			byte[] cache = new byte[(int) FSConfig.BLOCK_SIZE];
207 | 			try {
208 | 				int len = 0;
209 | 				String[] fromArr = this.from.split(FSConfig.INFO_SEP);
210 | 				String[] fromContent = fromArr[FSConfig.CONTENT_INDEX]
211 | 						.split(FSConfig.CONTENT_SEP);
212 | 				String[] toContent = this.to.split(FSConfig.INFO_SEP)[FSConfig.CONTENT_INDEX]
213 | 						.split(FSConfig.CONTENT_SEP);
214 | 				for (int i = 1; i < fromContent.length; i++) {
215 | 					if (i < toContent.length) {
216 | 						if (toContent[i].equals(fromContent[i])) {
217 | 							continue;
218 | 						}
219 | 					}
220 | 					long pos = FSConfig.BLOCK_SIZE * (i - 1);
221 | 					in.seek(pos);
222 | 					len = in.read(cache);
223 | 					out.seek(pos);
224 | 					out.write(cache, 0, len);
225 | 					listener_sync.work(len);
226 | 				}
227 | 				out.setLength(from.length());
228 | 				out.close();
229 | 				out = null;
230 | 				to.setLastModified(from.lastModified());
231 | 				FSConfig.cache(this.from, targetFile);
232 | 			} finally {
233 | 				in.close();
234 | 				if (null != out) {
235 | 					out.close();
236 | 				}
237 | 			}
238 | 		}
239 | 	}
240 | 
241 | }
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/main/Main.java:
--------------------------------------------------------------------------------
  1 | package com.hjh.file.sync.main;
  2 | 
  3 | import java.io.File;
  4 | import java.io.IOException;
  5 | import java.util.List;
  6 | 
  7 | import com.hjh.file.sync.core.FSConfig;
  8 | import com.hjh.file.sync.core.SyncFolderInfo;
  9 | import com.hjh.file.sync.core.SyncItem;
 10 | import com.hjh.file.sync.process.CancelControl;
 11 | import com.hjh.file.sync.process.IProcessListener;
 12 | import com.hjh.file.sync.process.SimpleProcessListener;
 13 | import com.hjh.file.sync.util.FileUtils;
 14 | import com.hjh.file.sync.util.LogHelper;
 15 | 
 16 | /**
 17 |  * 程序入口
 18 |  * 
 19 |  * @author 洪 qq:2260806429
 20 |  */
 21 | public class Main {
 22 | 
 23 | 	public static void main(String argv[]) throws IOException {
 24 | 		File sourceFile = null;
 25 | 		File targetFile = null;
 26 | 		if (argv.length == 2) {
 27 | 			sourceFile = new File(argv[0]);
 28 | 			targetFile = new File(argv[1]);
 29 | 		} else {
 30 | 			LogHelper.info("请传入参数");
 31 | 			return;
 32 | 		}
 33 | 		if (!sourceFile.exists()) {
 34 | 			LogHelper.info("源对象不存在:" + sourceFile.getAbsolutePath());
 35 | 			return;
 36 | 		}
 37 | 		long start = System.currentTimeMillis();
 38 | 		try {
 39 | 			sync(sourceFile, targetFile);
 40 | 		} finally {
 41 | 			long cost = System.currentTimeMillis() - start;
 42 | 			try {
 43 | 				Thread.sleep(500);
 44 | 			} catch (InterruptedException e) {
 45 | 			}
 46 | 			LogHelper.info("同步完成耗时:" + printCostTime(cost));
 47 | 		}
 48 | 	}
 49 | 
 50 | 	public static String printCostTime(long cost) {
 51 | 		if (cost < 1000) {
 52 | 			return cost + "毫秒";
 53 | 		}
 54 | 		cost = cost / 1000;
 55 | 		if (cost < 60) {
 56 | 			return cost + "秒";
 57 | 		}
 58 | 		if (cost / 60 < 10) {
 59 | 			return cost / 60 + "分" + cost % 60 + "秒";
 60 | 		}
 61 | 		return cost / 60 / 60 + "小时" + cost / 60 % 60 + "分" + cost % 60 % 60
 62 | 				+ "秒";
 63 | 	}
 64 | 
 65 | 	public static void sync(File sourceFile, File targetFile)
 66 | 			throws IOException {
 67 | 		final CancelControl cancelControl = new CancelControl();
 68 | 		LogHelper.info("Sync from \"" + sourceFile.getAbsolutePath()
 69 | 				+ "\" TO \"" + targetFile.getAbsolutePath() + "\"");
 70 | 		final IProcessListener listener_source = new SimpleProcessListener(
 71 | 				cancelControl);
 72 | 		listener_source.print("source scan");
 73 | 		File cacheFile = new File(sourceFile, FSConfig.CACHE_ID_FILE);
 74 | 		if (cacheFile.isDirectory()) {
 75 | 			FileUtils.del(cacheFile);
 76 | 		}
 77 | 		final SyncFolderInfo source = new SyncFolderInfo(sourceFile);
 78 | 
 79 | 		final IProcessListener listener_target = new SimpleProcessListener(
 80 | 				cancelControl);
 81 | 		listener_target.print("target scan");
 82 | 		final SyncFolderInfo target = new SyncFolderInfo(targetFile);
 83 | 
 84 | 		final int[] finishcount = new int[1];
 85 | 
 86 | 		new Thread() {
 87 | 			public void run() {
 88 | 				try {
 89 | 					source.scan(listener_source);
 90 | 				} catch (IOException e) {
 91 | 					LogHelper.error(e);
 92 | 					cancelControl.cancel = true;
 93 | 				} finally {
 94 | 					finishcount[0]++;
 95 | 				}
 96 | 			}
 97 | 		}.start();
 98 | 
 99 | 		new Thread() {
100 | 			public void run() {
101 | 				try {
102 | 					target.scan(listener_target);
103 | 				} catch (IOException e) {
104 | 					LogHelper.error(e);
105 | 					cancelControl.cancel = true;
106 | 				} finally {
107 | 					finishcount[0]++;
108 | 				}
109 | 			}
110 | 		}.start();
111 | 
112 | 		while (finishcount[0] != 2) {
113 | 			try {
114 | 				Thread.sleep(1000);
115 | 			} catch (InterruptedException e) {
116 | 				LogHelper.error(e);
117 | 			}
118 | 		}
119 | 
120 | 		try {
121 | 			Thread.sleep(500);
122 | 		} catch (InterruptedException e) {
123 | 			LogHelper.error(e);
124 | 		}
125 | 
126 | 		long size = 0;
127 | 		List items = source.sync(target);
128 | 		for (SyncItem item : items) {
129 | 			size += item.getSyncSize();
130 | 		}
131 | 		LogHelper.info("total sync size:"
132 | 				+ (size * 100 / (1024 * 1024) * 1.0 / 100) + "M");
133 | 		if (!targetFile.exists()) {
134 | 			targetFile.mkdir();
135 | 		}
136 | 		if (!targetFile.exists()) {
137 | 			throw new RuntimeException("创建目标文件夹失败");
138 | 		}
139 | 
140 | 		final IProcessListener listener_sync = new SimpleProcessListener(
141 | 				cancelControl, size);
142 | 		listener_sync.print("sync");
143 | 		for (SyncItem item : items) {
144 | 			if (listener_sync.isCancel()) {
145 | 				break;
146 | 			}
147 | 			item.sync(sourceFile, targetFile, listener_sync);
148 | 		}
149 | 
150 | 	}
151 | 
152 | }
153 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/process/CancelControl.java:
--------------------------------------------------------------------------------
1 | package com.hjh.file.sync.process;
2 | 
3 | public class CancelControl {
4 | 	public boolean cancel = false;
5 | 
6 | 	public boolean isCancel() {
7 | 		return cancel;
8 | 	}
9 | }
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/process/IProcessListener.java:
--------------------------------------------------------------------------------
 1 | package com.hjh.file.sync.process;
 2 | 
 3 | /**
 4 |  * 
 5 |  * 进度监听
 6 |  * 
 7 |  * @author 洪 qq:2260806429
 8 |  * 
 9 |  */
10 | public interface IProcessListener {
11 | 
12 | 	public void updateTotalSize(long totalSize);
13 | 
14 | 	public void work(long size);
15 | 
16 | 	public boolean isFinish();
17 | 
18 | 	public boolean isCancel();
19 | 
20 | 	public double getPercent();
21 | 
22 | 	public void print(String name);
23 | 
24 | }
25 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/process/ProcessPrinter.java:
--------------------------------------------------------------------------------
 1 | package com.hjh.file.sync.process;
 2 | 
 3 | import com.hjh.file.sync.util.LogHelper;
 4 | 
 5 | /**
 6 |  * @author 洪 qq:2260806429
 7 |  */
 8 | class ProcessPrinter {
 9 | 
10 | 	public boolean done;
11 | 	public String name;
12 | 
13 | 	public ProcessPrinter(String name) {
14 | 		this.name = name;
15 | 	}
16 | 
17 | 	public void start(final IProcessListener listener) {
18 | 		new Thread() {
19 | 			public void run() {
20 | 				done = false;
21 | 				try {
22 | 					do {
23 | 						LogHelper.info(name + " work:"
24 | 								+ String.format("%5.2f", listener.getPercent())
25 | 								+ "%");
26 | 						if (listener.isFinish()) {
27 | 							break;
28 | 						}
29 | 						try {
30 | 							Thread.sleep(500);
31 | 						} catch (InterruptedException e) {
32 | 							LogHelper.error(e);
33 | 						}
34 | 					} while (true);
35 | 				} finally {
36 | 					done = true;
37 | 				}
38 | 			}
39 | 		}.start();
40 | 	}
41 | 
42 | }
43 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/process/SimpleProcessListener.java:
--------------------------------------------------------------------------------
 1 | package com.hjh.file.sync.process;
 2 | 
 3 | /**
 4 |  * @author 洪 qq:2260806429
 5 |  */
 6 | public class SimpleProcessListener implements IProcessListener {
 7 | 
 8 | 	private CancelControl cancelControl;
 9 | 	private String name = "";
10 | 	private long totalsize;
11 | 	private long worksize = 0;
12 | 
13 | 	public SimpleProcessListener() {
14 | 		this(null);
15 | 	}
16 | 
17 | 	public SimpleProcessListener(CancelControl cancelControl) {
18 | 		this(cancelControl, 0);
19 | 	}
20 | 
21 | 	public SimpleProcessListener(CancelControl cancelControl, long totalSize) {
22 | 		this.cancelControl = cancelControl;
23 | 		this.totalsize = totalSize;
24 | 	}
25 | 
26 | 	@Override
27 | 	public void work(long size) {
28 | 		worksize += size;
29 | 	}
30 | 
31 | 	@Override
32 | 	public boolean isFinish() {
33 | 		return (totalsize != 0 && totalsize == worksize) || isCancel();
34 | 	}
35 | 
36 | 	@Override
37 | 	public double getPercent() {
38 | 		return ((int) (worksize * 1.0 / totalsize * 10000)) * 1.0 / 100;
39 | 	}
40 | 
41 | 	@Override
42 | 	public boolean isCancel() {
43 | 		return cancelControl == null ? false : cancelControl.isCancel();
44 | 	}
45 | 
46 | 	@Override
47 | 	public void updateTotalSize(long totalSize) {
48 | 		this.totalsize = totalSize;
49 | 		touchPrint();
50 | 	}
51 | 
52 | 	@Override
53 | 	public void print(String name) {
54 | 		this.name = name;
55 | 		touchPrint();
56 | 	}
57 | 
58 | 	private void touchPrint() {
59 | 		if (null != name && 0 != name.length() && 0 != this.totalsize) {
60 | 			new ProcessPrinter(name).start(this);
61 | 		}
62 | 	}
63 | 
64 | }
65 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/util/FileUtils.java:
--------------------------------------------------------------------------------
 1 | package com.hjh.file.sync.util;
 2 | 
 3 | import java.io.File;
 4 | 
 5 | public class FileUtils {
 6 | 
 7 | 	public static void del(File file) {
 8 | 		if (!file.exists()) {
 9 | 			return;
10 | 		}
11 | 		if (file.isDirectory()) {
12 | 			File list[] = file.listFiles();
13 | 			if (null != list) {
14 | 				for (File item : list) {
15 | 					del(item);
16 | 				}
17 | 			}
18 | 		}
19 | 		if (!file.delete()) {
20 | 			LogHelper.warn("删除文件失败:" + file.getAbsolutePath());
21 | 		}
22 | 	}
23 | 
24 | }
25 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/util/LogHelper.java:
--------------------------------------------------------------------------------
 1 | package com.hjh.file.sync.util;
 2 | 
 3 | /**
 4 |  * 日志工具
 5 |  * @author 洪 qq:2260806429
 6 |  */
 7 | public class LogHelper {
 8 | 
 9 | 	public static void info(String msg) {
10 | 		System.out.println("[INFO]" + msg);
11 | 	}
12 | 
13 | 	public static void warn(String msg) {
14 | 		System.out.println("[WARN]" + msg);
15 | 	}
16 | 
17 | 	public static void error(Exception e) {
18 | 		e.printStackTrace();
19 | 	}
20 | 
21 | }
22 | 
--------------------------------------------------------------------------------
/FileSync/src/com/hjh/file/sync/util/MD5.java:
--------------------------------------------------------------------------------
 1 | package com.hjh.file.sync.util;
 2 | 
 3 | import java.security.MessageDigest;
 4 | import java.security.NoSuchAlgorithmException;
 5 | 
 6 | /**
 7 |  * @author 洪 qq:2260806429
 8 |  */
 9 | public class MD5 {
10 | 
11 | 	public static String md5(byte[] datas, int pos, int len) {
12 | 		try {
13 | 			MessageDigest md = MessageDigest.getInstance("MD5");
14 | 			md.update(datas, pos, len);
15 | 			return toString(md);
16 | 		} catch (NoSuchAlgorithmException e) {
17 | 			throw new RuntimeException(e);
18 | 		}
19 | 	}
20 | 
21 | 	private static String toString(MessageDigest md) {
22 | 		byte b[] = md.digest();
23 | 		int i;
24 | 		StringBuffer buf = new StringBuffer("");
25 | 		for (int offset = 0; offset < b.length; offset++) {
26 | 			i = b[offset];
27 | 			if (i < 0)
28 | 				i += 256;
29 | 			if (i < 16)
30 | 				buf.append("0");
31 | 			buf.append(Integer.toHexString(i));
32 | 		}
33 | 		return buf.toString();
34 | 	}
35 | 
36 | }
37 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # file-sync
 2 | 数据同步工具
 3 | 
 4 | 一.问题背景
 5 | 经常碰到要同步数据的情况,而系统自带的复制功能又不能实现增量同步,每次都要做全量复制,发生异常情况后只能重头再来,非常麻烦,优其是对那种大文件的处理,更是耗时。
 6 | 二.解決方案
 7 | 1.计算源目录数据指纹
 8 | 2.计算目标目录数据指纹
 9 | 3.对比指纹数据,找出差异项,得到需要添加,删除或更新的文件列表,计算出需要更新的数据大小
10 | 4.挨个同步差异项,如果碰到大文件,则缓存其指纹数据到目标文件夹中,供下次同步数据时使用
11 | 三.数据指纹说明
12 | 数据指纹顾名思义,就是对某一文件夹或文件的唯一标识,其格式为:
13 | 文件相对路径+:(分隔符)+修改日期+:+数据长度+:+內容指纹
14 | 內容指纹是由多个内容块的md5组成
15 | 内容块就是对大文件进行分割处理,每次比较数据,最小的同步对象就是內容块,避免对整个文件做处理,也是实现增量同步的关健点
16 | 
17 | 
18 | 
19 | 
20 | 
21 | 使用说明:
22 | 1.安装Jre
23 | 2.下载fileSync.jar
24 | 3.运行命令:java -jar fileSync.jar 源目录 目标目录
25 | 
26 | 特性说明:
27 | 可增量同步,程序会检测源目录和目标目录已经同步好的数据,
28 | 对比之后继续同步未同步的数据
29 | 
30 | 
31 | 
32 | 
--------------------------------------------------------------------------------
/fileSync.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xxonehjh/file-sync/1c8fcc4b576d8a461a038d0c4066d15e883574da/fileSync.jar
--------------------------------------------------------------------------------
/run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xxonehjh/file-sync/1c8fcc4b576d8a461a038d0c4066d15e883574da/run.png
--------------------------------------------------------------------------------