├── 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 --------------------------------------------------------------------------------