001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.operations.down; 019 020import java.io.File; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.logging.Level; 027import java.util.logging.Logger; 028 029import org.syncany.config.Config; 030import org.syncany.database.FileVersion; 031import org.syncany.database.FileVersion.FileStatus; 032import org.syncany.database.FileVersionComparator; 033import org.syncany.database.FileVersionComparator.FileChange; 034import org.syncany.database.FileVersionComparator.FileVersionComparison; 035import org.syncany.database.MemoryDatabase; 036import org.syncany.database.PartialFileHistory; 037import org.syncany.database.PartialFileHistory.FileHistoryId; 038import org.syncany.database.SqlDatabase; 039import org.syncany.operations.Assembler; 040import org.syncany.operations.ChangeSet; 041import org.syncany.operations.down.actions.ChangeFileSystemAction; 042import org.syncany.operations.down.actions.DeleteFileSystemAction; 043import org.syncany.operations.down.actions.FileSystemAction; 044import org.syncany.operations.down.actions.NewFileSystemAction; 045import org.syncany.operations.down.actions.NewSymlinkFileSystemAction; 046import org.syncany.operations.down.actions.RenameFileSystemAction; 047import org.syncany.operations.down.actions.SetAttributesFileSystemAction; 048 049 050/** 051 * Implements the file synchronization algorithm in the down operation. 052 * 053 * The algorithm compares the local file on the disk with the last local 054 * database file version and the last winning file version and determines 055 * what file system action (fsa) to apply. 056 * 057 * Input variables: 058 * - winning version 059 * - winning file (= local file of winning version) 060 * - local version 061 * - local file (= local file of local version) 062 * 063 * Algorithm: 064 * if (has no local version) { 065 * compwinfwinv = compare winning file to winning version (incl. checksum!) 066 * 067 * if (compwinfwinv: winning file matches winning version) { 068 * // do nothing 069 * } 070 * else if (compwinfwinv: new) { 071 * add new fsa for winning version 072 * add multichunks to download list for winning version 073 * } 074 * else if (compwinfwinv: deleted) { 075 * add delete fsa for winning version 076 * } 077 * else if (compwinfwinv: changed link) { 078 * add changed link fsa for winning version 079 * } 080 * else if (compwinfwinv: changes attrs / modified date) { // does not(!) include "path" 081 * add changed attrs fsa for winning version 082 * } 083 * else if (compwinfwinv: changed path) { 084 * // Cannot be! 085 * } 086 * else { // size/checksum (path cannot be!) 087 * add conflict fsa for winning file 088 * add new fsa for winning version 089 * add multichunks to download list for winning version 090 * } 091 * } 092 * 093 * else { // local version exists 094 * complocflocv = compare local file to local version (incl. checksum!) 095 * 096 * if (complocflocv: local file matches local version) { // file as expected on disk 097 * complocvwinv = compare local version to winning version 098 * 099 * if (complocvwinv: local version matches winning version) { // means: local file = local version = winning version 100 * // Nothing to do 101 * } 102 * else if (complocvwinv: new) { 103 * // Cannot be! 104 * } 105 * else if (complocvwinv: deleted) { 106 * add delete fsa for winning version 107 * } 108 * else if (complocvwinv: changed link) { 109 * add changed link fsa for winning version 110 * } 111 * else if (complocvwinv: changes attrs / modified date / path) { // includes "path!" 112 * add changed attrs / renamed fsa for winning version 113 * } 114 * else { // size/checksum 115 * add changed fsa for winning version (and delete local version) 116 * add multichunks to download list for winning version 117 * } 118 * } 119 * else { // local file does NOT match local version 120 * if (local file exists) { 121 * add conflict fsa for local version 122 * } 123 * 124 * add new fsa for winning version 125 * add multichunks to download list for winning version 126 * } 127 * 128 */ 129public class FileSystemActionReconciliator { 130 private static final Logger logger = Logger.getLogger(FileSystemActionReconciliator.class.getSimpleName()); 131 132 private Config config; 133 private ChangeSet changeSet; 134 private SqlDatabase localDatabase; 135 private FileVersionComparator fileVersionComparator; 136 private Assembler assembler; 137 138 public FileSystemActionReconciliator(Config config, ChangeSet changeSet) { 139 this.config = config; 140 this.changeSet = changeSet; 141 this.localDatabase = new SqlDatabase(config); 142 this.fileVersionComparator = new FileVersionComparator(config.getLocalDir(), config.getChunker().getChecksumAlgorithm()); 143 } 144 145 public List<FileSystemAction> determineFileSystemActions(MemoryDatabase winnersDatabase) throws Exception { 146 List<PartialFileHistory> localFileHistoriesWithLastVersion = localDatabase.getFileHistoriesWithLastVersion(); 147 return determineFileSystemActions(winnersDatabase, false, localFileHistoriesWithLastVersion); 148 } 149 150 public List<FileSystemAction> determineFileSystemActions(MemoryDatabase winnersDatabase, boolean cleanupOccurred, 151 List<PartialFileHistory> localFileHistoriesWithLastVersion) throws Exception { 152 this.assembler = new Assembler(config, localDatabase, winnersDatabase); 153 154 List<FileSystemAction> fileSystemActions = new ArrayList<FileSystemAction>(); 155 156 // Load file history cache 157 logger.log(Level.INFO, "- Loading current file tree..."); 158 Map<FileHistoryId, FileVersion> localFileHistoryIdCache = fillFileHistoryIdCache(localFileHistoriesWithLastVersion); 159 160 logger.log(Level.INFO, "- Determine filesystem actions ..."); 161 162 for (PartialFileHistory winningFileHistory : winnersDatabase.getFileHistories()) { 163 // Get remote file version and content 164 FileVersion winningLastVersion = winningFileHistory.getLastVersion(); 165 File winningLastFile = new File(config.getLocalDir(), winningLastVersion.getPath()); 166 167 // Get local file version and content 168 FileVersion localLastVersion = localFileHistoryIdCache.get(winningFileHistory.getFileHistoryId()); 169 File localLastFile = (localLastVersion != null) ? new File(config.getLocalDir(), localLastVersion.getPath()) : null; 170 171 logger.log(Level.INFO, " + Comparing local version: "+localLastVersion); 172 logger.log(Level.INFO, " with winning version : "+winningLastVersion); 173 174 // Sync algorithm //// 175 176 // No local file version in local database 177 if (localLastVersion == null) { 178 determineActionNoLocalLastVersion(winningLastVersion, winningLastFile, winnersDatabase, fileSystemActions); 179 } 180 181 // Local version found in local database 182 else { 183 FileVersionComparison localFileToVersionComparison = fileVersionComparator.compare(localLastVersion, localLastFile, true); 184 185 // Local file on disk as expected 186 if (localFileToVersionComparison.areEqual()) { 187 determineActionWithLocalVersionAndLocalFileAsExpected(winningLastVersion, winningLastFile, localLastVersion, localLastFile, 188 winnersDatabase, fileSystemActions); 189 } 190 191 // Local file NOT what was expected 192 else { 193 determineActionWithLocalVersionAndLocalFileDiffers(winningLastVersion, winningLastFile, localLastVersion, localLastFile, 194 winnersDatabase, fileSystemActions, localFileToVersionComparison); 195 } 196 } 197 } 198 199 // Find file histories that are in the local database and not in the 200 // winner's database. They will be assumed to be deleted. 201 202 if (cleanupOccurred) { 203 logger.log(Level.INFO, "- Determine filesystem actions (for deleted histories in winner's branch)..."); 204 Map<FileHistoryId, FileVersion> winnerFileHistoryIdCache = fillFileHistoryIdCache(winnersDatabase.getFileHistories()); 205 206 for (PartialFileHistory localFileHistoryWithLastVersion : localFileHistoriesWithLastVersion) { 207 boolean localFileHistoryInWinnersDatabase = winnerFileHistoryIdCache.get(localFileHistoryWithLastVersion.getFileHistoryId()) != null; 208 209 // If the file history is also present in the winner's database, it 210 // has already been processed above. So we'll ignore it here. 211 212 if (!localFileHistoryInWinnersDatabase) { 213 FileVersion localLastVersion = localFileHistoryWithLastVersion.getLastVersion(); 214 File localLastFile = (localLastVersion != null) ? new File(config.getLocalDir(), localLastVersion.getPath()) : null; 215 216 determineActionFileHistoryNotInWinnerBranch(localLastVersion, localLastFile, fileSystemActions); 217 } 218 } 219 } 220 221 return fileSystemActions; 222 } 223 224 private void determineActionNoLocalLastVersion(FileVersion winningLastVersion, File winningLastFile, MemoryDatabase winnersDatabase, 225 List<FileSystemAction> outFileSystemActions) throws Exception { 226 227 FileVersionComparison winningFileToVersionComparison = fileVersionComparator.compare(winningLastVersion, winningLastFile, true); 228 229 boolean contentChanged = winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_CHECKSUM) 230 || winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_SIZE); 231 232 if (winningFileToVersionComparison.areEqual()) { 233 logger.log(Level.INFO, " -> (1) Equals: Nothing to do, winning version equals winning file: "+winningLastVersion+" AND "+winningLastFile); 234 } 235 else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.DELETED)) { 236 FileSystemAction action = new NewFileSystemAction(config, winnersDatabase, assembler, winningLastVersion); 237 outFileSystemActions.add(action); 238 239 logger.log(Level.INFO, " -> (2) Deleted: Local file does NOT exist, but it should, winning version not known: "+winningLastVersion+" AND "+winningLastFile); 240 logger.log(Level.INFO, " -> "+action); 241 242 changeSet.getNewFiles().add(winningLastVersion.getPath()); 243 } 244 else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.NEW)) { 245 FileSystemAction action = new DeleteFileSystemAction(config, null, winningLastVersion, winnersDatabase); 246 outFileSystemActions.add(action); 247 248 logger.log(Level.INFO, " -> (3) New: winning version was deleted, but local exists, winning version = "+winningLastVersion+" at "+winningLastFile); 249 logger.log(Level.INFO, " -> "+action); 250 251 changeSet.getDeletedFiles().add(winningLastVersion.getPath()); 252 } 253 else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_LINK_TARGET)) { 254 FileSystemAction action = new NewSymlinkFileSystemAction(config, winningLastVersion, winnersDatabase); 255 outFileSystemActions.add(action); 256 257 logger.log(Level.INFO, " -> (4) Changed link target: winning file has a different link target: "+winningLastVersion+" AND "+winningLastFile); 258 logger.log(Level.INFO, " -> "+action); 259 260 changeSet.getNewFiles().add(winningLastVersion.getPath()); 261 } 262 else if (!contentChanged && (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_LAST_MOD_DATE) 263 || winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_ATTRIBUTES))) { 264 265 FileSystemAction action = new SetAttributesFileSystemAction(config, winningLastVersion, winnersDatabase); 266 outFileSystemActions.add(action); 267 268 logger.log(Level.INFO, " -> (5) Changed file attributes: winning file has different file attributes: "+winningLastVersion+" AND "+winningLastFile); 269 logger.log(Level.INFO, " -> "+action); 270 271 changeSet.getNewFiles().add(winningLastVersion.getPath()); 272 } 273 else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_PATH)) { 274 logger.log(Level.INFO, " -> (6) Changed path: winning file has a different path: "+winningLastVersion+" AND "+winningLastFile); 275 throw new Exception("What happend here?"); 276 } 277 else { // Content changed 278 FileSystemAction action = new NewFileSystemAction(config, winnersDatabase, assembler, winningLastVersion); 279 outFileSystemActions.add(action); 280 281 logger.log(Level.INFO, " -> (7) Content changed: Winning file differs from winning version: "+winningLastVersion+" AND "+winningLastFile); 282 logger.log(Level.INFO, " -> "+action); 283 284 changeSet.getNewFiles().add(winningLastVersion.getPath()); 285 } 286 } 287 288 private void determineActionWithLocalVersionAndLocalFileAsExpected(FileVersion winningLastVersion, File winningLastFile, 289 FileVersion localLastVersion, File localLastFile, MemoryDatabase winnersDatabase, List<FileSystemAction> fileSystemActions) { 290 291 FileVersionComparison winningVersionToLocalVersionComparison = fileVersionComparator.compare(winningLastVersion, localLastVersion); 292 293 boolean contentChanged = winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_CHECKSUM) 294 || winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_SIZE); 295 296 if (winningVersionToLocalVersionComparison.areEqual()) { // Local file = local version = winning version! 297 logger.log(Level.INFO, " -> (8) Equals: Nothing to do, local file equals local version equals winning version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 298 } 299 else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.DELETED)) { 300 FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion); 301 fileSystemActions.add(action); 302 303 logger.log(Level.INFO, " -> (9) Content changed: Local file does not exist, but it should: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 304 logger.log(Level.INFO, " -> "+action); 305 306 changeSet.getChangedFiles().add(winningLastVersion.getPath()); 307 } 308 else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.NEW)) { 309 FileSystemAction action = new DeleteFileSystemAction(config, localLastVersion, winningLastVersion, winnersDatabase); 310 fileSystemActions.add(action); 311 312 logger.log(Level.INFO, " -> (10) Local file exists, but should not: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 313 logger.log(Level.INFO, " -> "+action); 314 315 changeSet.getDeletedFiles().add(winningLastVersion.getPath()); 316 } 317 else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_LINK_TARGET)) { 318 FileSystemAction action = new NewSymlinkFileSystemAction(config, winningLastVersion, winnersDatabase); 319 fileSystemActions.add(action); 320 321 logger.log(Level.INFO, " -> (11) Changed link target: local file has a different link target: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 322 logger.log(Level.INFO, " -> "+action); 323 324 changeSet.getNewFiles().add(winningLastVersion.getPath()); 325 } 326 else if (!contentChanged && (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_LAST_MOD_DATE) 327 || winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_ATTRIBUTES) 328 || winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_PATH))) { 329 330 FileSystemAction action = new RenameFileSystemAction(config, localLastVersion, winningLastVersion, winnersDatabase); 331 fileSystemActions.add(action); 332 333 logger.log(Level.INFO, " -> (12) Rename / Changed file attributes: Local file has different file attributes: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 334 logger.log(Level.INFO, " -> "+action); 335 336 changeSet.getChangedFiles().add(winningLastVersion.getPath()); 337 } 338 else { // Content changed 339 FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion); 340 fileSystemActions.add(action); 341 342 logger.log(Level.INFO, " -> (13) Content changed: Local file differs from winning version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 343 logger.log(Level.INFO, " -> "+action); 344 345 changeSet.getChangedFiles().add(winningLastVersion.getPath()); 346 } 347 } 348 349 private void determineActionWithLocalVersionAndLocalFileDiffers(FileVersion winningLastVersion, File winningLastFile, 350 FileVersion localLastVersion, File localLastFile, MemoryDatabase winnersDatabase, List<FileSystemAction> fileSystemActions, 351 FileVersionComparison localFileToVersionComparison) { 352 353 if (localFileToVersionComparison.getFileChanges().contains(FileChange.DELETED)) { 354 boolean winningLastVersionDeleted = winningLastVersion.getStatus() == FileStatus.DELETED; 355 356 if (!winningLastVersionDeleted) { 357 FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion); 358 fileSystemActions.add(action); 359 360 logger.log(Level.INFO, " -> (14) Content changed: Local file does NOT exist, and winning version changed: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 361 logger.log(Level.INFO, " -> "+action); 362 363 changeSet.getChangedFiles().add(winningLastVersion.getPath()); 364 } 365 else { 366 logger.log(Level.INFO, " -> (15) Doing nothing: Local file does NOT exist, and winning version is marked DELETED: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 367 } 368 } 369 else { 370 FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, winningLastVersion, localLastVersion); 371 fileSystemActions.add(action); 372 373 logger.log(Level.INFO, " -> (16) Content changed: Local file differs from last version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion); 374 logger.log(Level.INFO, " -> "+action); 375 376 changeSet.getChangedFiles().add(winningLastVersion.getPath()); 377 } 378 } 379 380 private void determineActionFileHistoryNotInWinnerBranch(FileVersion localLastVersion, File localLastFile, List<FileSystemAction> fileSystemActions) { 381 // No local file version in local database 382 if (localLastVersion == null) { 383 throw new RuntimeException("This should not happen."); 384 } 385 386 // Local version found in local database 387 else { 388 FileSystemAction action = new DeleteFileSystemAction(config, localLastVersion, localLastVersion, null); 389 fileSystemActions.add(action); 390 391 logger.log(Level.INFO, " -> (17) Local file exists, but not in winner branch -> File was deleted remotely: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = (none)"); 392 logger.log(Level.INFO, " -> "+action); 393 394 changeSet.getDeletedFiles().add(localLastVersion.getPath()); 395 } 396 } 397 398 private Map<FileHistoryId, FileVersion> fillFileHistoryIdCache(Collection<PartialFileHistory> fileHistoriesWithLastVersion) { 399 Map<FileHistoryId, FileVersion> fileHistoryIdCache = new HashMap<FileHistoryId, FileVersion>(); 400 401 for (PartialFileHistory fileHistory : fileHistoriesWithLastVersion) { 402 fileHistoryIdCache.put(fileHistory.getFileHistoryId(), fileHistory.getLastVersion()); 403 } 404 405 return fileHistoryIdCache; 406 } 407}