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.up; 019 020import java.io.File; 021import java.io.IOException; 022import java.security.SecureRandom; 023import java.util.Collection; 024import java.util.Date; 025import java.util.List; 026import java.util.Queue; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import org.syncany.chunk.Chunk; 031import org.syncany.chunk.Deduper; 032import org.syncany.chunk.DeduperListener; 033import org.syncany.chunk.MultiChunk; 034import org.syncany.config.Config; 035import org.syncany.config.LocalEventBus; 036import org.syncany.database.ChunkEntry; 037import org.syncany.database.ChunkEntry.ChunkChecksum; 038import org.syncany.database.DatabaseVersion; 039import org.syncany.database.FileContent; 040import org.syncany.database.FileContent.FileChecksum; 041import org.syncany.database.FileVersion; 042import org.syncany.database.FileVersion.FileStatus; 043import org.syncany.database.FileVersion.FileType; 044import org.syncany.database.FileVersionComparator; 045import org.syncany.database.FileVersionComparator.FileProperties; 046import org.syncany.database.FileVersionComparator.FileVersionComparison; 047import org.syncany.database.MemoryDatabase; 048import org.syncany.database.MultiChunkEntry; 049import org.syncany.database.MultiChunkEntry.MultiChunkId; 050import org.syncany.database.PartialFileHistory; 051import org.syncany.database.PartialFileHistory.FileHistoryId; 052import org.syncany.database.SqlDatabase; 053import org.syncany.operations.daemon.messages.UpIndexChangesDetectedSyncExternalEvent; 054import org.syncany.operations.daemon.messages.UpIndexEndSyncExternalEvent; 055import org.syncany.operations.daemon.messages.UpIndexMidSyncExternalEvent; 056import org.syncany.operations.daemon.messages.UpIndexStartSyncExternalEvent; 057import org.syncany.util.EnvironmentUtil; 058import org.syncany.util.FileUtil; 059import org.syncany.util.StringUtil; 060 061/** 062 * The indexer combines the chunking process with the corresponding database 063 * lookups for the resulting chunks. It implements the deduplication mechanism 064 * of Syncany. 065 * 066 * <p>The class takes a list of files as input and uses the {@link Deduper} to 067 * break these files into individual chunks. By implementing the {@link DeduperListener}, 068 * it reacts on chunking events and creates a new database version (with the newly 069 * added/changed/removed files. This functionality is entirely implemented by the 070 * index() method. 071 * 072 * <p>The class uses the currently loaded {@link MemoryDatabase} as well as a potential 073 * dirty database into account. Lookups for chunks and file histories are performed 074 * on both databases. 075 * 076 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 077 */ 078public class Indexer { 079 private static final Logger logger = Logger.getLogger(Indexer.class.getSimpleName()); 080 081 private static final String DEFAULT_POSIX_PERMISSIONS_FILE = "rw-r--r--"; 082 private static final String DEFAULT_POSIX_PERMISSIONS_FOLDER = "rwxr-xr-x"; 083 private static final String DEFAULT_DOS_ATTRIBUTES = "--a-"; 084 085 private Config config; 086 private Deduper deduper; 087 private SqlDatabase localDatabase; 088 089 private LocalEventBus eventBus; 090 091 092 public Indexer(Config config, Deduper deduper) { 093 this.config = config; 094 this.deduper = deduper; 095 this.localDatabase = new SqlDatabase(config, true); 096 097 this.eventBus = LocalEventBus.getInstance(); 098 } 099 100 /** 101 * This method implements the index/deduplication functionality of Syncany. It uses a {@link Deduper} 102 * to break files down, compares them to the local database and creates a new {@link DatabaseVersion} 103 * as a result. 104 * 105 * <p>Depending on what has changed, the new database version will contain new instances of 106 * {@link PartialFileHistory}, {@link FileVersion}, {@link FileContent}, {@link ChunkEntry} and 107 * {@link MultiChunkEntry}. 108 * 109 * @param files List of files to be deduplicated 110 * @param deletedFiles List of files that have been deleted 111 * @param databaseVersionQueue Queue to which created databaseVersions are offered 112 * @throws IOException If the chunking/deduplication cannot read/process any of the files 113 */ 114 public void index(List<File> files, List<File> deletedFiles, Queue<DatabaseVersion> databaseVersionQueue) throws IOException { 115 if (!files.isEmpty()) { 116 indexWithNewFiles(files, deletedFiles, databaseVersionQueue); 117 } 118 else { 119 indexWithoutNewFiles(files, deletedFiles, databaseVersionQueue); 120 } 121 122 localDatabase.finalize(); 123 } 124 125 private void indexWithNewFiles(List<File> files, List<File> deletedFiles, Queue<DatabaseVersion> databaseVersionQueue) throws IOException { 126 boolean isFirstFile = true; 127 int filesCount = files.size(); 128 129 while (!files.isEmpty()) { 130 DatabaseVersion newDatabaseVersion = new DatabaseVersion(); 131 132 // Create the DeduperListener that will receive MultiChunks and store them in the DatabaseVersion object 133 DeduperListener deduperListener = new IndexerDeduperListener(newDatabaseVersion); 134 135 // Signal the start of indexing if we are about to deduplicate the first file 136 if (isFirstFile) { 137 deduperListener.onStart(files.size()); 138 removeDeletedFiles(newDatabaseVersion, deletedFiles); // Add deletions in first database version 139 140 isFirstFile = false; 141 } 142 143 // Find and index new files 144 deduper.deduplicate(files, deduperListener); 145 146 if (!newDatabaseVersion.getFileHistories().isEmpty()) { 147 logger.log(Level.FINE, "Processed new database version: " + newDatabaseVersion); 148 databaseVersionQueue.offer(newDatabaseVersion); 149 150 int remainingFilesCount = filesCount - files.size(); 151 eventBus.post(new UpIndexMidSyncExternalEvent(config.getLocalDir().toString(), filesCount, remainingFilesCount)); 152 } 153 //else { (comment-only else case) 154 // Just chunks and multichunks, no filehistory. Since this means the file was being 155 // written/vanished during operations, it makes no sense to upload it. If the user 156 // wants it indexed, Up can be run again. 157 //} 158 } 159 } 160 161 private void indexWithoutNewFiles(List<File> files, List<File> deletedFiles, Queue<DatabaseVersion> databaseVersionQueue) { 162 DatabaseVersion newDatabaseVersion = new DatabaseVersion(); 163 164 removeDeletedFiles(newDatabaseVersion, deletedFiles); 165 logger.log(Level.FINE, "Added database version with only deletions: " + newDatabaseVersion); 166 167 databaseVersionQueue.offer(newDatabaseVersion); 168 } 169 170 private void removeDeletedFiles(DatabaseVersion newDatabaseVersion, List<File> deletedFiles) { 171 logger.log(Level.FINER, "- Looking for deleted files ..."); 172 173 for (File deletedFile : deletedFiles) { 174 String path = FileUtil.getRelativeDatabasePath(config.getLocalDir(), deletedFile); 175 PartialFileHistory fileHistory = localDatabase.getFileHistoriesWithLastVersionByPath(path); 176 177 // Ignore this file history if it has been updated in this database version before (file probably renamed!) 178 if (newDatabaseVersion.getFileHistory(fileHistory.getFileHistoryId()) != null) { 179 continue; 180 } 181 182 // Check if file exists, remove if it doesn't 183 FileVersion lastLocalVersion = fileHistory.getLastVersion(); 184 File lastLocalVersionOnDisk = new File(config.getLocalDir() + File.separator + lastLocalVersion.getPath()); 185 186 // Ignore this file history if the last version is marked "DELETED" 187 if (lastLocalVersion.getStatus() == FileStatus.DELETED) { 188 continue; 189 } 190 191 // Add this file history if a new file with this name has been added (file type change) 192 PartialFileHistory newFileWithSameName = getFileHistoryByPathFromDatabaseVersion(newDatabaseVersion, fileHistory.getLastVersion() 193 .getPath()); 194 195 // If file has VANISHED, mark as DELETED 196 if (!FileUtil.exists(lastLocalVersionOnDisk) || newFileWithSameName != null) { 197 PartialFileHistory fileHistoryForDeletion = createFileHistoryForDeletion(fileHistory, lastLocalVersion); 198 newDatabaseVersion.addFileHistory(fileHistoryForDeletion); 199 200 logger.log(Level.FINER, " + Deleted: Adding DELETED version: {0}", fileHistoryForDeletion.getLastVersion()); 201 logger.log(Level.FINER, " based on: {0}", lastLocalVersion); 202 } 203 } 204 } 205 206 private PartialFileHistory createFileHistoryForDeletion(PartialFileHistory fileHistory, FileVersion lastLocalVersion) { 207 PartialFileHistory deletedFileHistory = new PartialFileHistory(fileHistory.getFileHistoryId()); 208 FileVersion deletedVersion = lastLocalVersion.clone(); 209 210 deletedVersion.setStatus(FileStatus.DELETED); 211 deletedVersion.setVersion(fileHistory.getLastVersion().getVersion() + 1); 212 deletedVersion.setUpdated(new Date()); 213 214 deletedFileHistory.addFileVersion(deletedVersion); 215 216 return deletedFileHistory; 217 } 218 219 private PartialFileHistory getFileHistoryByPathFromDatabaseVersion(DatabaseVersion databaseVersion, String path) { 220 // TODO [medium] Extremely performance intensive, because this is called inside a loop above. Implement better caching for database version!!! 221 222 for (PartialFileHistory fileHistory : databaseVersion.getFileHistories()) { 223 FileVersion lastVersion = fileHistory.getLastVersion(); 224 225 if (lastVersion.getStatus() != FileStatus.DELETED && lastVersion.getPath().equals(path)) { 226 return fileHistory; 227 } 228 } 229 230 return null; 231 } 232 233 private class IndexerDeduperListener implements DeduperListener { 234 private FileVersionComparator fileVersionComparator; 235 private SecureRandom secureRandom; 236 private DatabaseVersion newDatabaseVersion; 237 238 private ChunkEntry chunkEntry; 239 private MultiChunkEntry multiChunkEntry; 240 private FileContent fileContent; 241 242 private FileProperties startFileProperties; 243 private FileProperties endFileProperties; 244 245 public IndexerDeduperListener(DatabaseVersion newDatabaseVersion) { 246 247 this.fileVersionComparator = new FileVersionComparator(config.getLocalDir(), config.getChunker().getChecksumAlgorithm()); 248 this.secureRandom = new SecureRandom(); 249 this.newDatabaseVersion = newDatabaseVersion; 250 } 251 252 @Override 253 public boolean onFileFilter(File file) { 254 logger.log(Level.FINER, "- +File {0}", file); 255 256 startFileProperties = fileVersionComparator.captureFileProperties(file, null, false); 257 258 // Check if file has vanished 259 if (!startFileProperties.exists() || startFileProperties.isLocked()) { 260 logger.log(Level.FINER, "- /File: {0}", file); 261 logger.log(Level.INFO, " * NOT ADDING because file has VANISHED (exists = {0}) or is LOCKED (locked = {1}).", new Object[] { 262 startFileProperties.exists(), startFileProperties.isLocked() }); 263 264 resetFileEnd(); 265 return false; 266 } 267 268 // Content 269 if (startFileProperties.getType() == FileType.FILE) { 270 logger.log(Level.FINER, "- +FileContent: {0}", file); 271 fileContent = new FileContent(); 272 } 273 274 return true; 275 } 276 277 @Override 278 public boolean onFileStart(File file) { 279 boolean processFile = startFileProperties.getType() == FileType.FILE; // Ignore directories and symlinks! 280 281 // We could fire an event here, but firing for every file 282 // is very exhausting for the event bus. 283 284 return processFile; 285 } 286 287 @Override 288 public void onFileEnd(File file, byte[] rawFileChecksum) { 289 // Get file attributes (get them while file exists) 290 291 // Note: Do NOT move any File-methods (file.anything()) below the file.exists()-part, 292 // because the file could vanish! 293 294 FileChecksum fileChecksum = (rawFileChecksum != null) ? new FileChecksum(rawFileChecksum) : null; 295 endFileProperties = fileVersionComparator.captureFileProperties(file, fileChecksum, false); 296 297 // Check if file has vanished 298 boolean fileIsLocked = endFileProperties.isLocked(); 299 boolean fileVanished = !endFileProperties.exists(); 300 boolean fileHasChanged = startFileProperties.getSize() != endFileProperties.getSize() 301 || startFileProperties.getLastModified() != endFileProperties.getLastModified(); 302 303 if (fileVanished || fileIsLocked || fileHasChanged) { 304 logger.log(Level.FINER, "- /File: {0}", file); 305 logger.log(Level.INFO, " * NOT ADDING because file has VANISHED (" + !endFileProperties.exists() + "), is LOCKED (" 306 + endFileProperties.isLocked() + "), or has CHANGED (" + fileHasChanged + ")"); 307 308 resetFileEnd(); 309 return; 310 } 311 312 // If it's still there, add it to the database 313 addFileVersion(endFileProperties); 314 315 // Reset 316 resetFileEnd(); 317 } 318 319 private void addFileVersion(FileProperties fileProperties) { 320 if (fileProperties.getChecksum() != null) { 321 logger.log(Level.FINER, "- /File: {0} (checksum {1})", 322 new Object[] { fileProperties.getRelativePath(), fileProperties.getChecksum() }); 323 } 324 else { 325 logger.log(Level.FINER, "- /File: {0} (directory/symlink/0-byte-file)", fileProperties.getRelativePath()); 326 } 327 328 // 1. Determine if file already exists in database 329 PartialFileHistory lastFileHistory = guessLastFileHistory(fileProperties); 330 FileVersion lastFileVersion = (lastFileHistory != null) ? lastFileHistory.getLastVersion() : null; 331 332 // 2. If file type changed, "close" the old file history by adding a file version that deletes the old file/directory 333 PartialFileHistory deletedFileHistory = null; 334 335 if (lastFileVersion != null && lastFileVersion.getType() != fileProperties.getType()) { 336 logger.log(Level.FINER, " * Detected change in file type. Deleting old file and starting a new file history."); 337 338 deletedFileHistory = createFileHistoryForDeletion(lastFileHistory, lastFileVersion); 339 lastFileHistory = null; 340 lastFileVersion = null; 341 } 342 343 // 3. Create new file history/version 344 PartialFileHistory fileHistory = createNewFileHistory(lastFileHistory); 345 FileVersion fileVersion = createNewFileVersion(lastFileVersion, fileProperties); 346 347 // 4. Compare new and last version 348 FileProperties lastFileVersionProperties = fileVersionComparator.captureFileProperties(lastFileVersion); 349 FileVersionComparison lastToNewFileVersionComparison = fileVersionComparator.compare(fileProperties, lastFileVersionProperties, true); 350 351 boolean newVersionDiffersFromToLastVersion = !lastToNewFileVersionComparison.areEqual(); 352 353 if (newVersionDiffersFromToLastVersion) { 354 fileHistory.addFileVersion(fileVersion); 355 newDatabaseVersion.addFileHistory(fileHistory); 356 357 if (deletedFileHistory != null) { 358 newDatabaseVersion.addFileHistory(deletedFileHistory); 359 } 360 361 logger.log(Level.INFO, " * Added file version: " + fileVersion); 362 logger.log(Level.INFO, " based on file version: " + lastFileVersion); 363 364 fireHasChangesEvent(); 365 } 366 else { 367 logger.log(Level.INFO, " * NOT ADDING file version: " + fileVersion); 368 logger.log(Level.INFO, " b/c IDENTICAL prev.: " + lastFileVersion); 369 } 370 371 // 4. Add file content (if not a directory) 372 if (fileProperties.getChecksum() != null && fileContent != null) { 373 fileContent.setSize(fileProperties.getSize()); 374 fileContent.setChecksum(fileProperties.getChecksum()); 375 376 // Check if content already exists, throw gathered content away if it does! 377 FileContent existingContent = localDatabase.getFileContent(fileProperties.getChecksum(), false); 378 379 if (existingContent == null) { 380 newDatabaseVersion.addFileContent(fileContent); 381 } 382 else { 383 // Uses existing content (already in database); ref. by checksum 384 } 385 } 386 } 387 388 private void fireHasChangesEvent() { 389 boolean firstNewFileDetected = newDatabaseVersion.getFileHistories().size() == 1; 390 391 if (firstNewFileDetected) { // Only fires once! 392 eventBus.post(new UpIndexChangesDetectedSyncExternalEvent(config.getLocalDir().getAbsolutePath())); 393 } 394 } 395 396 private PartialFileHistory createNewFileHistory(PartialFileHistory lastFileHistory) { 397 if (lastFileHistory == null) { 398 FileHistoryId newFileHistoryId = FileHistoryId.secureRandomFileId(); 399 return new PartialFileHistory(newFileHistoryId); 400 } 401 else { 402 return new PartialFileHistory(lastFileHistory.getFileHistoryId()); 403 } 404 } 405 406 private FileVersion createNewFileVersion(FileVersion lastFileVersion, FileProperties fileProperties) { 407 FileVersion fileVersion = null; 408 409 // Version 410 if (lastFileVersion == null) { 411 fileVersion = new FileVersion(); 412 fileVersion.setVersion(1L); 413 fileVersion.setStatus(FileStatus.NEW); 414 } 415 else { 416 fileVersion = lastFileVersion.clone(); 417 fileVersion.setVersion(lastFileVersion.getVersion() + 1); 418 } 419 420 // Simple attributes 421 fileVersion.setPath(fileProperties.getRelativePath()); 422 fileVersion.setLinkTarget(fileProperties.getLinkTarget()); 423 fileVersion.setType(fileProperties.getType()); 424 fileVersion.setSize(fileProperties.getSize()); 425 fileVersion.setChecksum(fileProperties.getChecksum()); 426 fileVersion.setLastModified(new Date(fileProperties.getLastModified())); 427 fileVersion.setUpdated(new Date()); 428 429 // Permissions 430 if (EnvironmentUtil.isWindows()) { 431 fileVersion.setDosAttributes(fileProperties.getDosAttributes()); 432 433 if (fileVersion.getType() == FileType.FOLDER) { 434 fileVersion.setPosixPermissions(DEFAULT_POSIX_PERMISSIONS_FOLDER); 435 } 436 else { 437 fileVersion.setPosixPermissions(DEFAULT_POSIX_PERMISSIONS_FILE); 438 } 439 } 440 else if (EnvironmentUtil.isUnixLikeOperatingSystem()) { 441 fileVersion.setPosixPermissions(fileProperties.getPosixPermissions()); 442 fileVersion.setDosAttributes(DEFAULT_DOS_ATTRIBUTES); 443 } 444 445 // Status 446 if (lastFileVersion != null) { 447 if (fileVersion.getType() == FileType.FILE 448 && FileChecksum.fileChecksumEquals(fileVersion.getChecksum(), lastFileVersion.getChecksum())) { 449 450 fileVersion.setStatus(FileStatus.CHANGED); 451 } 452 else if (!fileVersion.getPath().equals(lastFileVersion.getPath())) { 453 fileVersion.setStatus(FileStatus.RENAMED); 454 } 455 else { 456 fileVersion.setStatus(FileStatus.CHANGED); 457 } 458 } 459 460 return fileVersion; 461 } 462 463 private void resetFileEnd() { 464 fileContent = null; 465 startFileProperties = null; 466 endFileProperties = null; 467 } 468 469 private PartialFileHistory guessLastFileHistory(FileProperties fileProperties) { 470 if (fileProperties.getType() == FileType.FILE) { 471 return guessLastFileHistoryForFile(fileProperties); 472 } 473 else if (fileProperties.getType() == FileType.SYMLINK) { 474 return guessLastFileHistoryForSymlink(fileProperties); 475 } 476 else if (fileProperties.getType() == FileType.FOLDER) { 477 return guessLastFileHistoryForFolder(fileProperties); 478 } 479 else { 480 throw new RuntimeException("This should not happen."); 481 } 482 } 483 484 private PartialFileHistory guessLastFileHistoryForSymlink(FileProperties fileProperties) { 485 return guessLastFileHistoryForFolderOrSymlink(fileProperties); 486 } 487 488 private PartialFileHistory guessLastFileHistoryForFolder(FileProperties fileProperties) { 489 return guessLastFileHistoryForFolderOrSymlink(fileProperties); 490 } 491 492 private PartialFileHistory guessLastFileHistoryForFolderOrSymlink(FileProperties fileProperties) { 493 PartialFileHistory lastFileHistory = localDatabase.getFileHistoriesWithLastVersionByPath(fileProperties.getRelativePath()); 494 495 if (lastFileHistory == null) { 496 logger.log(Level.FINER, " * No old file history found, starting new history (path: " + fileProperties.getRelativePath() + ", " 497 + fileProperties.getType() + ")"); 498 return null; 499 } 500 else { 501 logger.log(Level.FINER, 502 " * Found old file history " + lastFileHistory.getFileHistoryId() + " (by path: " + fileProperties.getRelativePath() 503 + "), " + fileProperties.getType() + ", appending new version."); 504 return lastFileHistory; 505 } 506 } 507 508 /** 509 * Tries to guess a matching file history, first by path and then by matching checksum. 510 * 511 * <p>If the path matches the path of an existing file in the database, the file history 512 * from the database is used, and a new file version is appended. If there is no file 513 * in the database with that path, checksums are compared. 514 * 515 * <p>If there are more than one file with the same checksum (potential matches), the file 516 * with the closest path is chosen. 517 */ 518 private PartialFileHistory guessLastFileHistoryForFile(FileProperties fileProperties) { 519 PartialFileHistory lastFileHistory = null; 520 521 // a) Try finding a file history for which the last version has the same path 522 lastFileHistory = localDatabase.getFileHistoriesWithLastVersionByPath(fileProperties.getRelativePath()); 523 524 // b) If that fails, try finding files with a matching checksum 525 if (lastFileHistory == null) { 526 if (fileProperties.getChecksum() != null) { 527 Collection<PartialFileHistory> fileHistoriesWithSameChecksum = localDatabase 528 .getFileHistoriesWithLastVersionByChecksumSizeAndModifiedDate(fileProperties.getChecksum().toString(), 529 fileProperties.getSize(), new Date(fileProperties.getLastModified())); 530 531 if (fileHistoriesWithSameChecksum != null && fileHistoriesWithSameChecksum.size() > 0) { 532 fileHistoriesWithSameChecksum.removeAll(newDatabaseVersion.getFileHistories()); 533 lastFileHistory = guessLastFileHistoryForFileWithMatchingChecksum(fileProperties, fileHistoriesWithSameChecksum); 534 } 535 } 536 537 if (lastFileHistory == null) { 538 logger.log(Level.FINER, " * No old file history found, starting new history (path: " + fileProperties.getRelativePath() 539 + ", checksum: " + fileProperties.getChecksum() + ")"); 540 return null; 541 } 542 else { 543 logger.log(Level.FINER, 544 " * Found old file history " + lastFileHistory.getFileHistoryId() + " (by checksum: " + fileProperties.getChecksum() 545 + "), appending new version."); 546 return lastFileHistory; 547 } 548 } 549 else { 550 logger.log(Level.FINER, 551 " * Found old file history " + lastFileHistory.getFileHistoryId() + " (by path: " + fileProperties.getRelativePath() 552 + "), appending new version."); 553 return lastFileHistory; 554 } 555 } 556 557 private PartialFileHistory guessLastFileHistoryForFileWithMatchingChecksum(FileProperties fileProperties, 558 Collection<PartialFileHistory> fileHistoriesWithSameChecksum) { 559 PartialFileHistory lastFileHistory = null; 560 561 // Check if they do not exist anymore --> assume it has moved! 562 // We choose the best fileHistory to base on as follows: 563 564 // 1. Ensure that it was modified at the same time and is the same size 565 // 2. Check the fileHistory was deleted and the file does not actually exists 566 // 3. Choose the one with the longest matching tail of the path to the new path 567 568 for (PartialFileHistory fileHistoryWithSameChecksum : fileHistoriesWithSameChecksum) { 569 FileVersion lastVersion = fileHistoryWithSameChecksum.getLastVersion(); 570 571 if (fileProperties.getLastModified() != lastVersion.getLastModified().getTime() || fileProperties.getSize() != lastVersion.getSize()) { 572 continue; 573 } 574 575 File lastVersionOnLocalDisk = new File(config.getLocalDir() + File.separator + lastVersion.getPath()); 576 577 if (lastVersion.getStatus() != FileStatus.DELETED && !FileUtil.exists(lastVersionOnLocalDisk)) { 578 if (lastFileHistory == null) { 579 lastFileHistory = fileHistoryWithSameChecksum; 580 } 581 else { 582 String filePath = fileProperties.getRelativePath(); 583 String currentPreviousPath = lastFileHistory.getLastVersion().getPath(); 584 String candidatePreviousPath = fileHistoryWithSameChecksum.getLastVersion().getPath(); 585 586 for (int i = 0; i < filePath.length(); i++) { 587 if (!filePath.regionMatches(filePath.length() - i, candidatePreviousPath, candidatePreviousPath.length() - i, i)) { 588 // The candidate no longer matches, take the current path. 589 break; 590 } 591 592 if (!filePath.regionMatches(filePath.length() - i, currentPreviousPath, currentPreviousPath.length() - i, i)) { 593 // The current previous path no longer matches, take the new candidate 594 lastFileHistory = fileHistoryWithSameChecksum; 595 break; 596 } 597 } 598 } 599 } 600 } 601 602 return lastFileHistory; 603 } 604 605 @Override 606 public void onMultiChunkOpen(MultiChunk multiChunk) { 607 logger.log(Level.FINER, "- +MultiChunk {0}", multiChunk.getId()); 608 multiChunkEntry = new MultiChunkEntry(multiChunk.getId(), 0); // size unknown so far 609 } 610 611 @Override 612 public void onMultiChunkWrite(MultiChunk multiChunk, Chunk chunk) { 613 logger.log(Level.FINER, "- Chunk > MultiChunk: {0} > {1}", new Object[] { StringUtil.toHex(chunk.getChecksum()), multiChunk.getId() }); 614 multiChunkEntry.addChunk(chunkEntry.getChecksum()); 615 } 616 617 @Override 618 public void onMultiChunkClose(MultiChunk multiChunk) { 619 logger.log(Level.FINER, "- /MultiChunk {0}", multiChunk.getId()); 620 621 multiChunkEntry.setSize(multiChunk.getSize()); 622 623 newDatabaseVersion.addMultiChunk(multiChunkEntry); 624 multiChunkEntry = null; 625 } 626 627 @Override 628 public File getMultiChunkFile(MultiChunkId multiChunkId) { 629 return config.getCache().getEncryptedMultiChunkFile(multiChunkId); 630 } 631 632 @Override 633 public MultiChunkId createNewMultiChunkId(Chunk firstChunk) { 634 byte[] newMultiChunkId = new byte[firstChunk.getChecksum().length]; 635 secureRandom.nextBytes(newMultiChunkId); 636 637 return new MultiChunkId(newMultiChunkId); 638 } 639 640 @Override 641 public void onFileAddChunk(File file, Chunk chunk) { 642 logger.log(Level.FINER, "- Chunk > FileContent: {0} > {1}", new Object[] { StringUtil.toHex(chunk.getChecksum()), file }); 643 fileContent.addChunk(new ChunkChecksum(chunk.getChecksum())); 644 } 645 646 @Override 647 public void onStart(int fileCount) { 648 eventBus.post(new UpIndexStartSyncExternalEvent(config.getLocalDir().getAbsolutePath(), fileCount)); 649 } 650 651 @Override 652 public void onFinish() { 653 eventBus.post(new UpIndexEndSyncExternalEvent(config.getLocalDir().getAbsolutePath())); 654 } 655 656 /** 657 * Checks if chunk already exists in all database versions 658 * Afterwards checks if chunk exists in new introduced database version. 659 */ 660 @Override 661 public boolean onChunk(Chunk chunk) { 662 ChunkChecksum chunkChecksum = new ChunkChecksum(chunk.getChecksum()); 663 chunkEntry = localDatabase.getChunk(chunkChecksum); 664 665 if (chunkEntry == null) { 666 chunkEntry = newDatabaseVersion.getChunk(chunkChecksum); 667 668 if (chunkEntry == null) { 669 logger.log(Level.FINER, "- Chunk new: {0}", chunkChecksum.toString()); 670 671 chunkEntry = new ChunkEntry(chunkChecksum, chunk.getSize()); 672 newDatabaseVersion.addChunk(chunkEntry); 673 674 return true; 675 } 676 } 677 678 logger.log(Level.FINER, "- Chunk exists: {0}", StringUtil.toHex(chunk.getChecksum())); 679 return false; 680 } 681 } 682}