001package co.codewizards.cloudstore.local.transport; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004 005import java.io.ByteArrayInputStream; 006import java.io.File; 007import java.io.FileInputStream; 008import java.io.FileOutputStream; 009import java.io.IOException; 010import java.io.InputStream; 011import java.io.RandomAccessFile; 012import java.net.MalformedURLException; 013import java.net.URISyntaxException; 014import java.net.URL; 015import java.nio.file.Files; 016import java.nio.file.Path; 017import java.nio.file.Paths; 018import java.nio.file.StandardCopyOption; 019import java.security.NoSuchAlgorithmException; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Date; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.TreeMap; 029import java.util.UUID; 030import java.util.WeakHashMap; 031 032import javax.jdo.FetchPlan; 033import javax.jdo.PersistenceManager; 034 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037 038import co.codewizards.cloudstore.core.config.Config; 039import co.codewizards.cloudstore.core.dto.ChangeSetDTO; 040import co.codewizards.cloudstore.core.dto.CopyModificationDTO; 041import co.codewizards.cloudstore.core.dto.DeleteModificationDTO; 042import co.codewizards.cloudstore.core.dto.DirectoryDTO; 043import co.codewizards.cloudstore.core.dto.FileChunkDTO; 044import co.codewizards.cloudstore.core.dto.ModificationDTO; 045import co.codewizards.cloudstore.core.dto.NormalFileDTO; 046import co.codewizards.cloudstore.core.dto.RepoFileDTO; 047import co.codewizards.cloudstore.core.dto.RepositoryDTO; 048import co.codewizards.cloudstore.core.dto.SymlinkDTO; 049import co.codewizards.cloudstore.core.dto.TempChunkFileDTO; 050import co.codewizards.cloudstore.core.dto.jaxb.TempChunkFileDTOIO; 051import co.codewizards.cloudstore.core.progress.LoggerProgressMonitor; 052import co.codewizards.cloudstore.core.progress.NullProgressMonitor; 053import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper; 054import co.codewizards.cloudstore.core.repo.local.LocalRepoManager; 055import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory; 056import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction; 057import co.codewizards.cloudstore.core.repo.transport.AbstractRepoTransport; 058import co.codewizards.cloudstore.core.repo.transport.DeleteModificationCollisionException; 059import co.codewizards.cloudstore.core.repo.transport.FileWriteStrategy; 060import co.codewizards.cloudstore.core.util.HashUtil; 061import co.codewizards.cloudstore.core.util.IOUtil; 062import co.codewizards.cloudstore.local.FilenameFilterSkipMetaDir; 063import co.codewizards.cloudstore.local.LocalRepoSync; 064import co.codewizards.cloudstore.local.LocalRepoTransactionImpl; 065import co.codewizards.cloudstore.local.persistence.CopyModification; 066import co.codewizards.cloudstore.local.persistence.DeleteModification; 067import co.codewizards.cloudstore.local.persistence.DeleteModificationDAO; 068import co.codewizards.cloudstore.local.persistence.Directory; 069import co.codewizards.cloudstore.local.persistence.FileChunk; 070import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepo; 071import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepoDAO; 072import co.codewizards.cloudstore.local.persistence.LocalRepository; 073import co.codewizards.cloudstore.local.persistence.LocalRepositoryDAO; 074import co.codewizards.cloudstore.local.persistence.Modification; 075import co.codewizards.cloudstore.local.persistence.ModificationDAO; 076import co.codewizards.cloudstore.local.persistence.NormalFile; 077import co.codewizards.cloudstore.local.persistence.RemoteRepository; 078import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDAO; 079import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequest; 080import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequestDAO; 081import co.codewizards.cloudstore.local.persistence.RepoFile; 082import co.codewizards.cloudstore.local.persistence.RepoFileDAO; 083import co.codewizards.cloudstore.local.persistence.Symlink; 084 085public class FileRepoTransport extends AbstractRepoTransport { 086 private static final Logger logger = LoggerFactory.getLogger(FileRepoTransport.class); 087 088 private static final long MAX_REMOTE_REPOSITORY_REQUESTS_QUANTITY = 100; // TODO make configurable! 089 090 private static final String TEMP_CHUNK_FILE_PREFIX = "chunk_"; 091 private static final String TEMP_CHUNK_FILE_DTO_FILE_SUFFIX = ".xml"; 092 093 private LocalRepoManager localRepoManager; 094 095 @Override 096 public void close() { 097 if (localRepoManager != null) { 098 logger.debug("close: Closing localRepoManager."); 099 localRepoManager.close(); 100 } else 101 logger.debug("close: There is no localRepoManager."); 102 103 super.close(); 104 } 105 106 @Override 107 public UUID getRepositoryId() { 108 return getLocalRepoManager().getRepositoryId(); 109 } 110 111 @Override 112 public byte[] getPublicKey() { 113 return getLocalRepoManager().getPublicKey(); 114 } 115 116 @Override 117 public void requestRepoConnection(byte[] publicKey) { 118 assertNotNull("publicKey", publicKey); 119 UUID clientRepositoryId = getClientRepositoryIdOrFail(); 120 LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 121 try { 122 RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class); 123 RemoteRepository remoteRepository = remoteRepositoryDAO.getRemoteRepository(clientRepositoryId); 124 if (remoteRepository != null) 125 throw new IllegalArgumentException("RemoteRepository already connected! repositoryId=" + clientRepositoryId); 126 127 String localPathPrefix = getPathPrefix(); 128 RemoteRepositoryRequestDAO remoteRepositoryRequestDAO = transaction.getDAO(RemoteRepositoryRequestDAO.class); 129 RemoteRepositoryRequest remoteRepositoryRequest = remoteRepositoryRequestDAO.getRemoteRepositoryRequest(clientRepositoryId); 130 if (remoteRepositoryRequest != null) { 131 logger.info("RemoteRepository already requested to be connected. repositoryId={}", clientRepositoryId); 132 133 // For security reasons, we do not allow to modify the public key! If we did, 134 // an attacker might replace the public key while the user is verifying the public key's 135 // fingerprint. The user would see & confirm the old public key, but the new public key 136 // would be written to the RemoteRepository. This requires really lucky timing, but 137 // if the attacker surveils the user, this might be feasable. 138 if (!Arrays.equals(remoteRepositoryRequest.getPublicKey(), publicKey)) 139 throw new IllegalStateException("Cannot modify the public key! Use 'dropRepoConnection' to drop the old request or wait until it expired."); 140 141 // For the same reasons stated above, we do not allow changing the local path-prefix, too. 142 if (!remoteRepositoryRequest.getLocalPathPrefix().equals(localPathPrefix)) 143 throw new IllegalStateException("Cannot modify the local path-prefix! Use 'dropRepoConnection' to drop the old request or wait until it expired."); 144 145 remoteRepositoryRequest.setChanged(new Date()); // make sure it is not deleted soon (the request expires after a while) 146 } 147 else { 148 long remoteRepositoryRequestsCount = remoteRepositoryRequestDAO.getObjectsCount(); 149 if (remoteRepositoryRequestsCount >= MAX_REMOTE_REPOSITORY_REQUESTS_QUANTITY) 150 throw new IllegalStateException(String.format( 151 "The maximum number of connection requests (%s) is reached or exceeded! Please retry later, when old requests were accepted or expired.", MAX_REMOTE_REPOSITORY_REQUESTS_QUANTITY)); 152 153 remoteRepositoryRequest = new RemoteRepositoryRequest(); 154 remoteRepositoryRequest.setRepositoryId(clientRepositoryId); 155 remoteRepositoryRequest.setPublicKey(publicKey); 156 remoteRepositoryRequest.setLocalPathPrefix(localPathPrefix); 157 remoteRepositoryRequestDAO.makePersistent(remoteRepositoryRequest); 158 } 159 160 transaction.commit(); 161 } finally { 162 transaction.rollbackIfActive(); 163 } 164 } 165 166 @Override 167 public RepositoryDTO getRepositoryDTO() { 168 LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction(); 169 try { 170 LocalRepositoryDAO localRepositoryDAO = transaction.getDAO(LocalRepositoryDAO.class); 171 LocalRepository localRepository = localRepositoryDAO.getLocalRepositoryOrFail(); 172 RepositoryDTO repositoryDTO = toRepositoryDTO(localRepository); 173 transaction.commit(); 174 return repositoryDTO; 175 } finally { 176 transaction.rollbackIfActive(); 177 } 178 } 179 180 @Override 181 public ChangeSetDTO getChangeSetDTO(final boolean localSync) { 182 if (localSync) 183 getLocalRepoManager().localSync(new LoggerProgressMonitor(logger)); 184 185 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 186 final ChangeSetDTO changeSetDTO = new ChangeSetDTO(); 187 final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); // It writes the LastSyncToRemoteRepo! 188 try { 189 LocalRepositoryDAO localRepositoryDAO = transaction.getDAO(LocalRepositoryDAO.class); 190 RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class); 191 LastSyncToRemoteRepoDAO lastSyncToRemoteRepoDAO = transaction.getDAO(LastSyncToRemoteRepoDAO.class); 192 ModificationDAO modificationDAO = transaction.getDAO(ModificationDAO.class); 193 RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class); 194 195 // We must *first* read the LocalRepository and afterwards all changes, because this way, we don't need to lock it in the DB. 196 // If we *then* read RepoFiles with a newer localRevision, it doesn't do any harm - we'll simply read them again, in the 197 // next run. 198 LocalRepository localRepository = localRepositoryDAO.getLocalRepositoryOrFail(); 199 changeSetDTO.setRepositoryDTO(toRepositoryDTO(localRepository)); 200 201 RemoteRepository toRemoteRepository = remoteRepositoryDAO.getRemoteRepositoryOrFail(clientRepositoryId); 202 203 LastSyncToRemoteRepo lastSyncToRemoteRepo = lastSyncToRemoteRepoDAO.getLastSyncToRemoteRepo(toRemoteRepository); 204 if (lastSyncToRemoteRepo == null) { 205 lastSyncToRemoteRepo = new LastSyncToRemoteRepo(); 206 lastSyncToRemoteRepo.setRemoteRepository(toRemoteRepository); 207 lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(-1); 208 } 209 lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(localRepository.getRevision()); 210 lastSyncToRemoteRepoDAO.makePersistent(lastSyncToRemoteRepo); 211 212 ((LocalRepoTransactionImpl)transaction).getPersistenceManager().getFetchPlan().setGroup(FetchPlan.ALL); 213 Collection<Modification> modifications = modificationDAO.getModificationsAfter(toRemoteRepository, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()); 214 changeSetDTO.setModificationDTOs(toModificationDTOs(modifications)); 215 216 if (!getPathPrefix().isEmpty()) { 217 Collection<DeleteModification> deleteModifications = transaction.getDAO(DeleteModificationDAO.class).getDeleteModificationsForPathOrParentOfPathAfter( 218 getPathPrefix(), lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), toRemoteRepository); 219 if (!deleteModifications.isEmpty()) { // our virtual root was deleted => create synthetic DeleteModificationDTO for virtual root 220 DeleteModificationDTO deleteModificationDTO = new DeleteModificationDTO(); 221 deleteModificationDTO.setId(0); 222 deleteModificationDTO.setLocalRevision(localRepository.getRevision()); 223 deleteModificationDTO.setPath(""); 224 changeSetDTO.getModificationDTOs().add(deleteModificationDTO); 225 } 226 } 227 228 final Collection<RepoFile> repoFiles = repoFileDAO.getRepoFilesChangedAfterExclLastSyncFromRepositoryId( 229 lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), clientRepositoryId); 230 RepoFile pathPrefixRepoFile = null; // the virtual root for the client 231 if (!getPathPrefix().isEmpty()) { 232 pathPrefixRepoFile = repoFileDAO.getRepoFile(getLocalRepoManager().getLocalRoot(), getPathPrefixFile()); 233 } 234 Map<Long, RepoFileDTO> id2RepoFileDTO = getId2RepoFileDTOWithParents(pathPrefixRepoFile, repoFiles, repoFileDAO); 235 changeSetDTO.setRepoFileDTOs(new ArrayList<RepoFileDTO>(id2RepoFileDTO.values())); 236 237 transaction.commit(); 238 return changeSetDTO; 239 } finally { 240 transaction.rollbackIfActive(); 241 } 242 } 243 244 protected File getPathPrefixFile() { 245 String pathPrefix = getPathPrefix(); 246 if (pathPrefix.isEmpty()) 247 return getLocalRepoManager().getLocalRoot(); 248 else 249 return new File(getLocalRepoManager().getLocalRoot(), pathPrefix); 250 } 251 252 @Override 253 public void makeDirectory(String path, Date lastModified) { 254 path = prefixPath(path); 255 File file = getFile(path); 256 UUID clientRepositoryId = getClientRepositoryIdOrFail(); 257 LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 258 try { 259 assertNoDeleteModificationCollision(transaction, clientRepositoryId, path); 260 mkDir(transaction, clientRepositoryId, file, lastModified); 261 transaction.commit(); 262 } finally { 263 transaction.rollbackIfActive(); 264 } 265 } 266 267 @Override 268 public void makeSymlink(String path, String target, Date lastModified) { 269 path = prefixPath(path); 270 assertNotNull("target", target); 271 final File file = getFile(path); 272 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 273 final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 274 try { 275 final RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class); 276 277 final File parentFile = file.getParentFile(); 278 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 279 try { 280 assertNoDeleteModificationCollision(transaction, clientRepositoryId, path); 281 282 if (file.exists() && !isSymlink(file)) 283 file.renameTo(IOUtil.createCollisionFile(file)); 284 285 if (file.exists() && !isSymlink(file)) 286 throw new IllegalStateException("Could not rename file! It is still in the way: " + file); 287 288 final File localRoot = getLocalRepoManager().getLocalRoot(); 289 290 try { 291 final boolean currentTargetEqualsNewTarget; 292 final Path symlinkPath = file.toPath(); 293 if (Files.isSymbolicLink(file.toPath()) || file.exists()) { 294 final Path currentTargetPath = Files.readSymbolicLink(symlinkPath); 295 final String currentTarget = IOUtil.toPathString(currentTargetPath); 296 currentTargetEqualsNewTarget = currentTarget.equals(target); 297 if (!currentTargetEqualsNewTarget) { 298 final RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file); 299 if (repoFile == null) { 300 File collisionFile = IOUtil.createCollisionFile(file); 301 file.renameTo(collisionFile); 302 if (file.exists()) 303 throw new IllegalStateException("Could not rename file to resolve collision: " + file); 304 } 305 else 306 detectAndHandleFileCollision(transaction, clientRepositoryId, parentFile, repoFile); 307 308 file.delete(); 309 } 310 } 311 else 312 currentTargetEqualsNewTarget = false; 313 314 if (!currentTargetEqualsNewTarget) 315 Files.createSymbolicLink(symlinkPath, Paths.get(target)); 316 317 if (lastModified != null) 318 IOUtil.setLastModifiedNoFollow(file, lastModified.getTime()); 319 320 } catch (IOException e) { 321 throw new RuntimeException(e); 322 } 323 324 new LocalRepoSync(transaction).sync(file, new NullProgressMonitor()); 325 326 Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles = getOffset2TempChunkFileWithDTOFile(file).values(); 327 for (TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) { 328 if (tempChunkFileWithDTOFile.getTempChunkFileDTOFile() != null) 329 deleteOrFail(tempChunkFileWithDTOFile.getTempChunkFileDTOFile()); 330 331 if (tempChunkFileWithDTOFile.getTempChunkFile() != null) 332 deleteOrFail(tempChunkFileWithDTOFile.getTempChunkFile()); 333 } 334 335 final RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file); 336 if (repoFile == null) 337 throw new IllegalStateException("LocalRepoSync.sync(...) did not create the RepoFile for file: " + file); 338 339 if (!(repoFile instanceof Symlink)) 340 throw new IllegalStateException("LocalRepoSync.sync(...) created an instance of " + repoFile.getClass().getName() + " instead of a Symlink for file: " + file); 341 342 repoFile.setLastSyncFromRepositoryId(clientRepositoryId); 343 344 } finally { 345 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 346 } 347 348 transaction.commit(); 349 } finally { 350 transaction.rollbackIfActive(); 351 } 352 } 353 354 private void assertNoDeleteModificationCollision(LocalRepoTransaction transaction, UUID fromRepositoryId, String path) throws DeleteModificationCollisionException { 355 RemoteRepository fromRemoteRepository = transaction.getDAO(RemoteRepositoryDAO.class).getRemoteRepositoryOrFail(fromRepositoryId); 356 long lastSyncFromRemoteRepositoryLocalRevision = fromRemoteRepository.getLocalRevision(); 357 358 if (!path.startsWith("/")) 359 path = '/' + path; 360 361 DeleteModificationDAO deleteModificationDAO = transaction.getDAO(DeleteModificationDAO.class); 362 Collection<DeleteModification> deleteModifications = deleteModificationDAO.getDeleteModificationsForPathOrParentOfPathAfter( 363 path, lastSyncFromRemoteRepositoryLocalRevision, fromRemoteRepository); 364 365 if (!deleteModifications.isEmpty()) 366 throw new DeleteModificationCollisionException( 367 String.format("There is at least one DeleteModification for repositoryId=%s path='%s'", fromRepositoryId, path)); 368 } 369 370 @Override 371 public void copy(String fromPath, String toPath) { 372 fromPath = prefixPath(fromPath); 373 toPath = prefixPath(toPath); 374 375 final File fromFile = getFile(fromPath); 376 final File toFile = getFile(toPath); 377 378 if (!fromFile.exists()) // TODO throw an exception and catch in RepoToRepoSync! 379 return; 380 381 if (toFile.exists()) // TODO either simply throw an exception or implement proper collision check. 382 return; 383 384 final File toParentFile = toFile.getParentFile(); 385 final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 386 try { 387 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(toParentFile); 388 try { 389 try { 390 if (!toParentFile.isDirectory()) 391 toParentFile.mkdirs(); 392 393 Files.copy(fromFile.toPath(), toFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES); 394 } catch (IOException e) { 395 throw new RuntimeException(e); 396 } 397 398 final LocalRepoSync localRepoSync = new LocalRepoSync(transaction); 399 final RepoFile toRepoFile = localRepoSync.sync(toFile, new NullProgressMonitor()); 400 assertNotNull("toRepoFile", toRepoFile); 401 toRepoFile.setLastSyncFromRepositoryId(getClientRepositoryIdOrFail()); 402 } finally { 403 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(toParentFile); 404 } 405 transaction.commit(); 406 } finally { 407 transaction.rollbackIfActive(); 408 } 409 } 410 411 @Override 412 public void move(String fromPath, String toPath) { 413 fromPath = prefixPath(fromPath); 414 toPath = prefixPath(toPath); 415 416 final File fromFile = getFile(fromPath); 417 final File toFile = getFile(toPath); 418 419 if (!fromFile.exists()) // TODO throw an exception and catch in RepoToRepoSync! 420 return; 421 422 if (toFile.exists()) // TODO either simply throw an exception or implement proper collision check. 423 return; 424 425 final File fromParentFile = fromFile.getParentFile(); 426 final File toParentFile = toFile.getParentFile(); 427 LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 428 try { 429 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(fromParentFile); 430 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(toParentFile); 431 try { 432 try { 433 if (!toParentFile.isDirectory()) 434 toParentFile.mkdirs(); 435 436 Files.move(fromFile.toPath(), toFile.toPath()); 437 } catch (IOException e) { 438 throw new RuntimeException(e); 439 } 440 441 final LocalRepoSync localRepoSync = new LocalRepoSync(transaction); 442 final RepoFile toRepoFile = localRepoSync.sync(toFile, new NullProgressMonitor()); 443 final RepoFile fromRepoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fromFile); 444 if (fromRepoFile != null) 445 localRepoSync.deleteRepoFile(fromRepoFile); 446 447 assertNotNull("toRepoFile", toRepoFile); 448 toRepoFile.setLastSyncFromRepositoryId(getClientRepositoryIdOrFail()); 449 } finally { 450 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(fromParentFile); 451 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(toParentFile); 452 } 453 transaction.commit(); 454 } finally { 455 transaction.rollbackIfActive(); 456 } 457 } 458 459 @Override 460 public void delete(String path) { 461 path = prefixPath(path); 462 File file = getFile(path); 463 UUID clientRepositoryId = getClientRepositoryIdOrFail(); 464 boolean fileIsLocalRoot = localRepoManager.getLocalRoot().equals(file); 465 File parentFile = file.getParentFile(); 466 LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 467 try { 468 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 469 try { 470 LocalRepoSync localRepoSync = new LocalRepoSync(transaction); 471 localRepoSync.sync(file, new NullProgressMonitor()); 472 473 if (fileIsLocalRoot) { 474 // Cannot delete the repository's root! Deleting all its contents instead. 475 long fileLastModified = file.lastModified(); 476 try { 477 File[] children = file.listFiles(new FilenameFilterSkipMetaDir()); 478 if (children == null) 479 throw new IllegalStateException("File-listing localRoot returned null: " + file); 480 481 for (File child : children) 482 delete(transaction, localRepoSync, clientRepositoryId, child); 483 } finally { 484 file.setLastModified(fileLastModified); 485 } 486 } 487 else 488 delete(transaction, localRepoSync, clientRepositoryId, file); 489 490 } finally { 491 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 492 } 493 transaction.commit(); 494 } finally { 495 transaction.rollbackIfActive(); 496 } 497 } 498 499 private void delete(LocalRepoTransaction transaction, LocalRepoSync localRepoSync, UUID fromRepositoryId, File file) { 500 if (detectFileCollisionRecursively(transaction, fromRepositoryId, file)) { 501 file.renameTo(IOUtil.createCollisionFile(file)); 502 503 if (file.exists()) 504 throw new IllegalStateException("Renaming file failed: " + file); 505 } 506 507 if (!IOUtil.deleteDirectoryRecursively(file)) { 508 throw new IllegalStateException("Deleting file or directory failed: " + file); 509 } 510 511 RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), file); 512 if (repoFile != null) 513 localRepoSync.deleteRepoFile(repoFile); 514 } 515 516 @Override 517 public RepoFileDTO getRepoFileDTO(String path) { 518 RepoFileDTO repoFileDTO = null; 519 path = prefixPath(path); 520 File file = getFile(path); 521 LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); // it performs a local sync! 522 try { 523 LocalRepoSync localRepoSync = new LocalRepoSync(transaction); 524 localRepoSync.sync(file, new NullProgressMonitor()); 525 526 RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class); 527 RepoFile repoFile = repoFileDAO.getRepoFile(getLocalRepoManager().getLocalRoot(), file); 528 if (repoFile != null) 529 repoFileDTO = toRepoFileDTO(repoFile, repoFileDAO, Integer.MAX_VALUE); // TODO pass depth as argument - or maybe leave it this way? 530 531 transaction.commit(); 532 } catch (RuntimeException x) { 533 throw x; 534 } catch (Exception x) { 535 throw new RuntimeException(x); 536 } finally { 537 transaction.rollbackIfActive(); 538 } 539 return repoFileDTO; 540 } 541 542 private FileChunkDTO toFileChunkDTO(FileChunk fileChunk) { 543 final FileChunkDTO fileChunkDTO = new FileChunkDTO(); 544 fileChunkDTO.setOffset(fileChunk.getOffset()); 545 fileChunkDTO.setLength(fileChunk.getLength()); 546 fileChunkDTO.setSha1(fileChunk.getSha1()); 547 return fileChunkDTO; 548 } 549 550 /** 551 * @param offset the offset in the (real) destination file (<i>not</i> in {@code tempChunkFile}! there the offset is always 0). 552 * @param tempChunkFile the tempChunkFile containing the chunk's data. Must not be <code>null</code>. 553 * @param sha1 the sha1 of the single chunk (in {@code tempChunkFile}). Must not be <code>null</code>. 554 * @return the DTO. Never <code>null</code>. 555 */ 556 private TempChunkFileDTO createTempChunkFileDTO(final long offset, final File tempChunkFile, final String sha1) { 557 assertNotNull("tempChunkFile", tempChunkFile); 558 assertNotNull("sha1", sha1); 559 560 if (!tempChunkFile.exists()) 561 throw new IllegalArgumentException("The tempChunkFile does not exist: " + tempChunkFile.getAbsolutePath()); 562 563 final FileChunkDTO fileChunkDTO = new FileChunkDTO(); 564 fileChunkDTO.setOffset(offset); 565 566 final long tempChunkFileLength = tempChunkFile.length(); 567 if (tempChunkFileLength > Integer.MAX_VALUE) 568 throw new IllegalStateException("tempChunkFile.length > Integer.MAX_VALUE"); 569 570 fileChunkDTO.setLength((int) tempChunkFileLength); 571 fileChunkDTO.setSha1(sha1); 572 573 final TempChunkFileDTO tempChunkFileDTO = new TempChunkFileDTO(); 574 tempChunkFileDTO.setFileChunkDTO(fileChunkDTO); 575 return tempChunkFileDTO; 576 } 577 578 protected LocalRepoManager getLocalRepoManager() { 579 if (localRepoManager == null) { 580 logger.debug("getLocalRepoManager: Creating a new LocalRepoManager."); 581 File remoteRootFile; 582 try { 583 remoteRootFile = new File(getRemoteRootWithoutPathPrefix().toURI()); 584 } catch (URISyntaxException e) { 585 throw new RuntimeException(e); 586 } 587 localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(remoteRootFile); 588 } 589 return localRepoManager; 590 } 591 592 @Override 593 protected URL determineRemoteRootWithoutPathPrefix() { 594 File remoteRootFile; 595 try { 596 remoteRootFile = new File(getRemoteRoot().toURI()); 597 } catch (URISyntaxException e) { 598 throw new RuntimeException(e); 599 } 600 601 File localRootFile = LocalRepoHelper.getLocalRootContainingFile(remoteRootFile); 602 if (localRootFile == null) 603 throw new IllegalStateException(String.format( 604 "remoteRoot='%s' does not point to a file or directory within an existing repository (nor its root directory)!", 605 getRemoteRoot())); 606 607 try { 608 return localRootFile.toURI().toURL(); 609 } catch (MalformedURLException e) { 610 throw new RuntimeException(e); 611 } 612 } 613 614 private List<ModificationDTO> toModificationDTOs(Collection<Modification> modifications) { 615 long startTimestamp = System.currentTimeMillis(); 616 List<ModificationDTO> result = new ArrayList<ModificationDTO>(assertNotNull("modifications", modifications).size()); 617 for (Modification modification : modifications) { 618 ModificationDTO modificationDTO = toModificationDTO(modification); 619 if (modificationDTO != null) 620 result.add(modificationDTO); 621 } 622 logger.debug("toModificationDTOs: Creating {} ModificationDTOs took {} ms.", result.size(), System.currentTimeMillis() - startTimestamp); 623 return result; 624 } 625 626 private ModificationDTO toModificationDTO(Modification modification) { 627 ModificationDTO modificationDTO; 628 if (modification instanceof CopyModification) { 629 CopyModification copyModification = (CopyModification) modification; 630 631 String fromPath = copyModification.getFromPath(); 632 String toPath = copyModification.getToPath(); 633 if (!isPathUnderPathPrefix(fromPath) || !isPathUnderPathPrefix(toPath)) 634 return null; 635 636 fromPath = unprefixPath(fromPath); 637 toPath = unprefixPath(toPath); 638 639 CopyModificationDTO copyModificationDTO = new CopyModificationDTO(); 640 modificationDTO = copyModificationDTO; 641 copyModificationDTO.setFromPath(fromPath); 642 copyModificationDTO.setToPath(toPath); 643 } 644 else if (modification instanceof DeleteModification) { 645 DeleteModification deleteModification = (DeleteModification) modification; 646 647 String path = deleteModification.getPath(); 648 if (!isPathUnderPathPrefix(path)) 649 return null; 650 651 path = unprefixPath(path); 652 653 DeleteModificationDTO deleteModificationDTO; 654 modificationDTO = deleteModificationDTO = new DeleteModificationDTO(); 655 deleteModificationDTO.setPath(path); 656 } 657 else 658 throw new IllegalArgumentException("Unknown modification type: " + modification); 659 660 modificationDTO.setId(modification.getId()); 661 modificationDTO.setLocalRevision(modification.getLocalRevision()); 662 663 return modificationDTO; 664 } 665 666 private RepositoryDTO toRepositoryDTO(LocalRepository localRepository) { 667 RepositoryDTO repositoryDTO = new RepositoryDTO(); 668 repositoryDTO.setRepositoryId(localRepository.getRepositoryId()); 669 repositoryDTO.setRevision(localRepository.getRevision()); 670 repositoryDTO.setPublicKey(localRepository.getPublicKey()); 671 return repositoryDTO; 672 } 673 674 private Map<Long, RepoFileDTO> getId2RepoFileDTOWithParents(RepoFile pathPrefixRepoFile, Collection<RepoFile> repoFiles, RepoFileDAO repoFileDAO) { 675 assertNotNull("repoFileDAO", repoFileDAO); 676 assertNotNull("repoFiles", repoFiles); 677 Map<Long, RepoFileDTO> entityID2RepoFileDTO = new HashMap<Long, RepoFileDTO>(); 678 for (RepoFile repoFile : repoFiles) { 679 RepoFile rf = repoFile; 680 if (rf instanceof NormalFile) { 681 NormalFile nf = (NormalFile) rf; 682 if (nf.isInProgress()) { 683 continue; 684 } 685 } 686 687 if (pathPrefixRepoFile != null && !isDirectOrIndirectParent(pathPrefixRepoFile, rf)) 688 continue; 689 690 while (rf != null) { 691 if (!entityID2RepoFileDTO.containsKey(rf.getId())) { 692 RepoFileDTO repoFileDTO = toRepoFileDTO(rf, repoFileDAO, 0); 693 if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) { 694 repoFileDTO.setParentId(null); // virtual root has no parent! 695 repoFileDTO.setName(""); // virtual root has no name! 696 } 697 698 entityID2RepoFileDTO.put(rf.getId(), repoFileDTO); 699 } 700 701 if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) 702 break; 703 704 rf = rf.getParent(); 705 } 706 } 707 return entityID2RepoFileDTO; 708 } 709 710 private boolean isDirectOrIndirectParent(RepoFile parentRepoFile, RepoFile repoFile) { 711 assertNotNull("parentRepoFile", parentRepoFile); 712 assertNotNull("repoFile", repoFile); 713 RepoFile rf = repoFile; 714 while (rf != null) { 715 if (parentRepoFile.equals(rf)) 716 return true; 717 718 rf = rf.getParent(); 719 } 720 return false; 721 } 722 723 private RepoFileDTO toRepoFileDTO(RepoFile repoFile, RepoFileDAO repoFileDAO, int depth) { 724 assertNotNull("repoFileDAO", repoFileDAO); 725 assertNotNull("repoFile", repoFile); 726 final RepoFileDTO repoFileDTO; 727 if (repoFile instanceof NormalFile) { 728 final NormalFile normalFile = (NormalFile) repoFile; 729 final NormalFileDTO normalFileDTO; 730 repoFileDTO = normalFileDTO = new NormalFileDTO(); 731 normalFileDTO.setLength(normalFile.getLength()); 732 normalFileDTO.setSha1(normalFile.getSha1()); 733 if (depth > 0) { 734 // TODO this should actually be a SortedSet, but for whatever reason, I started 735 // getting ClassCastExceptions and had to switch to a normal Set :-( 736 final List<FileChunk> fileChunks = new ArrayList<>(normalFile.getFileChunks()); 737 Collections.sort(fileChunks); 738 for (final FileChunk fileChunk : fileChunks) { 739 normalFileDTO.getFileChunkDTOs().add(toFileChunkDTO(fileChunk)); 740 } 741 } 742 if (depth > 1) { 743 final TempChunkFileDTOIO tempChunkFileDTOIO = new TempChunkFileDTOIO(); 744 final File file = repoFile.getFile(getLocalRepoManager().getLocalRoot()); 745 for (TempChunkFileWithDTOFile tempChunkFileWithDTOFile : getOffset2TempChunkFileWithDTOFile(file).values()) { 746 final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile(); 747 if (tempChunkFileDTOFile == null) 748 continue; // incomplete: meta-data not yet written => ignore 749 750 final TempChunkFileDTO tempChunkFileDTO; 751 try { 752 tempChunkFileDTO = tempChunkFileDTOIO.deserialize(tempChunkFileDTOFile); 753 } catch (Exception x) { 754 logger.warn("toRepoFileDTO: Ignoring corrupt tempChunkFileDTOFile '" + tempChunkFileDTOFile.getAbsolutePath() + "': " + x, x); 755 continue; 756 } 757 normalFileDTO.getTempFileChunkDTOs().add(assertNotNull("tempChunkFileDTO.fileChunkDTO", tempChunkFileDTO.getFileChunkDTO())); 758 } 759 } 760 } 761 else if (repoFile instanceof Directory) { 762 repoFileDTO = new DirectoryDTO(); 763 } 764 else if (repoFile instanceof Symlink) { 765 final Symlink symlink = (Symlink) repoFile; 766 final SymlinkDTO symlinkDTO; 767 repoFileDTO = symlinkDTO = new SymlinkDTO(); 768 symlinkDTO.setTarget(symlink.getTarget()); 769 } 770 else 771 throw new UnsupportedOperationException("RepoFile type not yet supported: " + repoFile); 772 773 repoFileDTO.setId(repoFile.getId()); 774 repoFileDTO.setLocalRevision(repoFile.getLocalRevision()); 775 repoFileDTO.setName(repoFile.getName()); 776 repoFileDTO.setParentId(repoFile.getParent() == null ? null : repoFile.getParent().getId()); 777 repoFileDTO.setLastModified(repoFile.getLastModified()); 778 779 return repoFileDTO; 780 } 781 782 private void mkDir(LocalRepoTransaction transaction, UUID clientRepositoryId, File file, Date lastModified) { 783 assertNotNull("transaction", transaction); 784 assertNotNull("file", file); 785 786 final File localRoot = getLocalRepoManager().getLocalRoot(); 787 if (localRoot.equals(file)) { 788 return; 789 } 790 791 final File parentFile = file.getParentFile(); 792 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 793 try { 794 RepoFile parentRepoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, parentFile); 795 796 if (!localRoot.equals(parentFile) && (!parentFile.isDirectory() || parentRepoFile == null)) 797 mkDir(transaction, clientRepositoryId, parentFile, null); 798 799 if (parentRepoFile == null) 800 parentRepoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, parentFile); 801 802 if (parentRepoFile == null) // now, it should definitely not be null anymore! 803 throw new IllegalStateException("parentRepoFile == null"); 804 805 if (file.exists() && !file.isDirectory()) 806 file.renameTo(IOUtil.createCollisionFile(file)); 807 808 if (file.exists() && !file.isDirectory()) 809 throw new IllegalStateException("Could not rename file! It is still in the way: " + file); 810 811 if (!file.isDirectory()) 812 file.mkdir(); 813 814 if (!file.isDirectory()) 815 throw new IllegalStateException("Could not create directory (permissions?!): " + file); 816 817 RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, file); 818 if (repoFile != null && !(repoFile instanceof Directory)) { 819 transaction.getDAO(RepoFileDAO.class).deletePersistent(repoFile); 820 repoFile = null; 821 } 822 823 if (lastModified != null) 824 file.setLastModified(lastModified.getTime()); 825 826 if (repoFile == null) { 827 Directory directory; 828 repoFile = directory = new Directory(); 829 directory.setName(file.getName()); 830 directory.setParent(parentRepoFile); 831 directory.setLastModified(new Date(file.lastModified())); 832 repoFile = directory = transaction.getDAO(RepoFileDAO.class).makePersistent(directory); 833 } 834 else if (repoFile.getLastModified().getTime() != file.lastModified()) 835 repoFile.setLastModified(new Date(file.lastModified())); 836 837 repoFile.setLastSyncFromRepositoryId(clientRepositoryId); 838 } finally { 839 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 840 } 841 } 842 843 /** 844 * @param path the prefixed path (relative to the real root). 845 * @return the file in the local repository. Never <code>null</code>. 846 */ 847 protected File getFile(String path) { 848 path = assertNotNull("path", path).replace('/', File.separatorChar); 849 File file = new File(getLocalRepoManager().getLocalRoot(), path); 850 return file; 851 } 852 853 @Override 854 public byte[] getFileData(String path, long offset, int length) { 855 path = prefixPath(path); 856 File file = getFile(path); 857 try { 858 RandomAccessFile raf = new RandomAccessFile(file, "r"); 859 try { 860 raf.seek(offset); 861 if (length < 0) { 862 long l = raf.length() - offset; 863 if (l > Integer.MAX_VALUE) 864 throw new IllegalArgumentException( 865 String.format("The data to be read from file '%s' is too large (offset=%s length=%s limit=%s). You must specify a length (and optionally an offset) to read it partially.", 866 path, offset, length, Integer.MAX_VALUE)); 867 868 length = (int) l; 869 } 870 871 byte[] bytes = new byte[length]; 872 int off = 0; 873 int numRead = 0; 874 while (off < bytes.length && (numRead = raf.read(bytes, off, bytes.length-off)) >= 0) { 875 off += numRead; 876 } 877 878 if (off < bytes.length) // Read INCOMPLETELY => discarding 879 return null; 880 881 return bytes; 882 } finally { 883 raf.close(); 884 } 885 } catch (IOException e) { 886 throw new RuntimeException(e); 887 } 888 } 889 890 @Override 891 public void beginPutFile(String path) { 892 path = prefixPath(path); 893 final File file = getFile(path); // null-check already inside getFile(...) - no need for another check here 894 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 895 final File parentFile = file.getParentFile(); 896 final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 897 try { 898 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 899 try { 900 if (isSymlink(file) || (file.exists() && !file.isFile())) { // exists() and isFile() both resolve symlinks! Their result depends on where the symlink points to. 901 logger.info("beginPutFile: Collision: Destination file already exists and is a symlink or a directory! file='{}'", file.getAbsolutePath()); 902 final File collisionFile = IOUtil.createCollisionFile(file); 903 file.renameTo(collisionFile); 904 new LocalRepoSync(transaction).sync(collisionFile, new NullProgressMonitor()); 905 } 906 907 if (isSymlink(file) || (file.exists() && !file.isFile())) 908 throw new IllegalStateException("Could not rename file! It is still in the way: " + file); 909 910 final File localRoot = getLocalRepoManager().getLocalRoot(); 911 assertNoDeleteModificationCollision(transaction, clientRepositoryId, path); 912 913 boolean newFile = false; 914 if (!file.isFile()) { 915 newFile = true; 916 try { 917 file.createNewFile(); 918 } catch (IOException e) { 919 throw new RuntimeException(e); 920 } 921 } 922 923 if (!file.isFile()) 924 throw new IllegalStateException("Could not create file (permissions?!): " + file); 925 926 // A complete sync run might take very long. Therefore, we better update our local meta-data 927 // *immediately* before beginning the sync of this file and before detecting a collision. 928 // Furthermore, maybe the file is new and there's no meta-data, yet, hence we must do this anyway. 929 final RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class); 930 new LocalRepoSync(transaction).sync(file, new NullProgressMonitor()); 931 932 deleteTempChunkFilesWithoutDTOFile(getOffset2TempChunkFileWithDTOFile(file).values()); 933 934 final RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file); 935 if (repoFile == null) 936 throw new IllegalStateException("LocalRepoSync.sync(...) did not create the RepoFile for file: " + file); 937 938 if (!(repoFile instanceof NormalFile)) 939 throw new IllegalStateException("LocalRepoSync.sync(...) created an instance of " + repoFile.getClass().getName() + " instead of a NormalFile for file: " + file); 940 941 final NormalFile normalFile = (NormalFile) repoFile; 942 943 if (!newFile && !normalFile.isInProgress()) 944 detectAndHandleFileCollision(transaction, clientRepositoryId, file, normalFile); 945 946 normalFile.setLastSyncFromRepositoryId(clientRepositoryId); 947 normalFile.setInProgress(true); 948 } finally { 949 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 950 } 951 transaction.commit(); 952 } finally { 953 transaction.rollbackIfActive(); 954 } 955 } 956 957 private boolean isSymlink(final File file) { 958 return Files.isSymbolicLink(file.toPath()); 959 } 960 961 /** 962 * Detect if the file to be copied has been modified locally (or copied from another repository) after the last 963 * sync from the repository identified by {@code fromRepositoryId}. 964 * <p> 965 * If there is a collision - i.e. the destination file has been modified, too - then the destination file is moved 966 * away by renaming it. The name to which it is renamed is created by {@link IOUtil#createCollisionFile(File)}. 967 * Afterwards the file is copied back to its original name. 968 * <p> 969 * The reason for renaming it first (instead of directly copying it) is that there might be open file handles. 970 * In GNU/Linux, the open file handles stay open and thus are then connected to the renamed file, thus continuing 971 * to modify the file which was moved away. In Windows, the renaming likely fails and we abort with an exception. 972 * In both cases, we do our best to avoid both processes from writing to the same file simultaneously without locking 973 * it. 974 * <p> 975 * In the future (this is NOT YET IMPLEMENTED), we might lock it in {@link #beginPutFile(String)} and 976 * keep the lock until {@link #endPutFile(String, Date, long, String)} or a timeout occurs - and refresh the lock 977 * (i.e. postpone the timeout) with every {@link #putFileData(String, long, byte[])}. The reason for this 978 * quite complicated strategy is that we cannot guarantee that the {@link #endPutFile(String, Date, long, String)} 979 * is ever invoked (the client might crash inbetween). We don't want a locked file to linger forever. 980 * 981 * @param transaction the DB transaction. Must not be <code>null</code>. 982 * @param fromRepositoryId the ID of the source repository from which the file is about to be copied. Must not be <code>null</code>. 983 * @param file the file that is to be copied (i.e. overwritten). Must not be <code>null</code>. 984 * @param normalFileOrSymlink the DB entity corresponding to {@code file}. Must not be <code>null</code>. 985 */ 986 private void detectAndHandleFileCollision(LocalRepoTransaction transaction, UUID fromRepositoryId, File file, RepoFile normalFileOrSymlink) { 987 if (detectFileCollision(transaction, fromRepositoryId, file, normalFileOrSymlink)) { 988 final File collisionFile = IOUtil.createCollisionFile(file); 989 file.renameTo(collisionFile); 990 if (file.exists()) 991 throw new IllegalStateException("Could not rename file to resolve collision: " + file); 992 993 try { 994 Files.copy(collisionFile.toPath(), file.toPath(), StandardCopyOption.COPY_ATTRIBUTES); 995 } catch (IOException e) { 996 throw new RuntimeException(e); 997 } 998 999 new LocalRepoSync(transaction).sync(collisionFile, new NullProgressMonitor()); // TODO sub-progress-monitor! 1000 } 1001 } 1002 1003 private boolean detectFileCollisionRecursively(LocalRepoTransaction transaction, UUID fromRepositoryId, File fileOrDirectory) { 1004 assertNotNull("transaction", transaction); 1005 assertNotNull("fromRepositoryId", fromRepositoryId); 1006 assertNotNull("fileOrDirectory", fileOrDirectory); 1007 1008 // we handle symlinks before invoking exists() below, because this method and most other File methods resolve symlinks! 1009 if (Files.isSymbolicLink(fileOrDirectory.toPath())) { 1010 RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fileOrDirectory); 1011 if (!(repoFile instanceof Symlink)) 1012 return true; // We had a change after the last local sync (symlink => directory or normal file)! 1013 1014 return detectFileCollision(transaction, fromRepositoryId, fileOrDirectory, repoFile); 1015 } 1016 1017 if (!fileOrDirectory.exists()) { // Is this correct? If it does not exist, then there is no collision? TODO what if it has been deleted locally and modified remotely and local is destination and that's our collision?! 1018 return false; 1019 } 1020 1021 if (fileOrDirectory.isFile()) { 1022 RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fileOrDirectory); 1023 if (!(repoFile instanceof NormalFile)) 1024 return true; // We had a change after the last local sync (normal file => directory or symlink)! 1025 1026 return detectFileCollision(transaction, fromRepositoryId, fileOrDirectory, repoFile); 1027 } 1028 1029 File[] children = fileOrDirectory.listFiles(); 1030 if (children == null) 1031 throw new IllegalStateException("listFiles() of directory returned null: " + fileOrDirectory); 1032 1033 for (File child : children) { 1034 if (detectFileCollisionRecursively(transaction, fromRepositoryId, child)) 1035 return true; 1036 } 1037 1038 return false; 1039 } 1040 1041 /** 1042 * Detect if the file to be copied or deleted has been modified locally (or copied from another repository) after the last 1043 * sync from the repository identified by {@code fromRepositoryId}. 1044 * @param transaction 1045 * @param fromRepositoryId 1046 * @param file 1047 * @param normalFileOrSymlink 1048 * @return <code>true</code>, if there is a collision; <code>false</code>, if there is none. 1049 */ 1050 private boolean detectFileCollision(LocalRepoTransaction transaction, UUID fromRepositoryId, File file, RepoFile normalFileOrSymlink) { 1051 assertNotNull("transaction", transaction); 1052 assertNotNull("fromRepositoryId", fromRepositoryId); 1053 assertNotNull("file", file); 1054 assertNotNull("normalFileOrSymlink", normalFileOrSymlink); 1055 1056 if (!file.exists()) { 1057 logger.debug("detectFileCollision: path='{}': return false, because destination file does not exist.", normalFileOrSymlink.getPath()); 1058 return false; 1059 } 1060 1061 RemoteRepository fromRemoteRepository = transaction.getDAO(RemoteRepositoryDAO.class).getRemoteRepositoryOrFail(fromRepositoryId); 1062 long lastSyncFromRemoteRepositoryLocalRevision = fromRemoteRepository.getLocalRevision(); 1063 if (normalFileOrSymlink.getLocalRevision() <= lastSyncFromRemoteRepositoryLocalRevision) { 1064 logger.debug("detectFileCollision: path='{}': return false, because: normalFileOrSymlink.localRevision <= lastSyncFromRemoteRepositoryLocalRevision :: {} <= {}", normalFileOrSymlink.getPath(), normalFileOrSymlink.getLocalRevision(), lastSyncFromRemoteRepositoryLocalRevision); 1065 return false; 1066 } 1067 1068 // The file was transferred from the same repository before and was thus not changed locally nor in another repo. 1069 // This can only happen, if the sync was interrupted (otherwise the check for the localRevision above 1070 // would have already caused this method to abort). 1071 if (fromRepositoryId.equals(normalFileOrSymlink.getLastSyncFromRepositoryId())) { 1072 logger.debug("detectFileCollision: path='{}': return false, because: fromRepositoryId == normalFileOrSymlink.lastSyncFromRepositoryId :: fromRepositoryId='{}'", normalFileOrSymlink.getPath(), fromRemoteRepository); 1073 return false; 1074 } 1075 1076 logger.debug("detectFileCollision: path='{}': return true! fromRepositoryId='{}' normalFileOrSymlink.localRevision={} lastSyncFromRemoteRepositoryLocalRevision={} normalFileOrSymlink.lastSyncFromRepositoryId='{}'", 1077 normalFileOrSymlink.getPath(), fromRemoteRepository, normalFileOrSymlink.getLocalRevision(), lastSyncFromRemoteRepositoryLocalRevision, normalFileOrSymlink.getLastSyncFromRepositoryId()); 1078 return true; 1079 } 1080 1081 @Override 1082 public void putFileData(String path, final long offset, final byte[] fileData) { 1083 path = prefixPath(path); 1084 final File file = getFile(path); 1085 final File parentFile = file.getParentFile(); 1086 final File localRoot = getLocalRepoManager().getLocalRoot(); 1087 final LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction(); // It writes into the file system, but it only reads from the DB. 1088 try { 1089 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 1090 try { 1091 RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, file); 1092 if (repoFile == null) 1093 throw new IllegalStateException("No RepoFile found for file: " + file); 1094 1095 if (!(repoFile instanceof NormalFile)) 1096 throw new IllegalStateException("RepoFile is not an instance of NormalFile for file: " + file); 1097 1098 NormalFile normalFile = (NormalFile) repoFile; 1099 if (!normalFile.isInProgress()) 1100 throw new IllegalStateException(String.format("NormalFile.inProgress == false! beginFile(...) not called?! repoFile=%s file=%s", 1101 repoFile, file)); 1102 1103 final FileWriteStrategy fileWriteStrategy = getFileWriteStrategy(file); 1104 logger.debug("putFileData: fileWriteStrategy={}", fileWriteStrategy); 1105 switch (fileWriteStrategy) { 1106 case directDuringTransfer: 1107 writeFileDataToDestFile(file, offset, fileData); 1108 break; 1109 case directAfterTransfer: 1110 case replaceAfterTransfer: 1111 writeFileDataToTempChunkFile(file, offset, fileData); 1112 break; 1113 default: 1114 throw new IllegalStateException("Unknown fileWriteStrategy: " + fileWriteStrategy); 1115 } 1116 } finally { 1117 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 1118 } 1119 transaction.commit(); 1120 } finally { 1121 transaction.rollbackIfActive(); 1122 } 1123 } 1124 1125 private void writeTempChunkFileToDestFile(final File destFile, final File tempChunkFile, final TempChunkFileDTO tempChunkFileDTO) { 1126 assertNotNull("destFile", destFile); 1127 assertNotNull("tempChunkFile", tempChunkFile); 1128 assertNotNull("tempChunkFileDTO", tempChunkFileDTO); 1129 final long offset = assertNotNull("tempChunkFileDTO.fileChunkDTO", tempChunkFileDTO.getFileChunkDTO()).getOffset(); 1130 final byte[] fileData = new byte[(int) tempChunkFile.length()]; 1131 try { 1132 final InputStream in = new FileInputStream(tempChunkFile); 1133 try { 1134 int off = 0; 1135 while (off < fileData.length) { 1136 final int bytesRead = in.read(fileData, off, fileData.length - off); 1137 if (bytesRead > 0) { 1138 off += bytesRead; 1139 } 1140 else if (bytesRead < 0) { 1141 throw new IllegalStateException("InputStream ended before expected file length!"); 1142 } 1143 } 1144 if (off > fileData.length || in.read() != -1) 1145 throw new IllegalStateException("InputStream contained more data than expected file length!"); 1146 } finally { 1147 in.close(); 1148 } 1149 } catch (IOException e) { 1150 throw new RuntimeException(e); 1151 } 1152 1153 final String sha1FromDTOFile = tempChunkFileDTO.getFileChunkDTO().getSha1(); 1154 final String sha1FromFileData = sha1(fileData); 1155 1156 logger.trace("writeTempChunkFileToDestFile: Read {} bytes with SHA1 '{}' from '{}'.", fileData.length, sha1FromFileData, tempChunkFile.getAbsolutePath()); 1157 1158 if (!sha1FromFileData.equals(sha1FromDTOFile)) 1159 throw new IllegalStateException("SHA1 mismatch! Corrupt temporary chunk file or corresponding DTO file: " + tempChunkFile.getAbsolutePath()); 1160 1161 writeFileDataToDestFile(destFile, offset, fileData); 1162 } 1163 1164 private void writeFileDataToDestFile(final File destFile, final long offset, final byte[] fileData) { 1165 assertNotNull("destFile", destFile); 1166 assertNotNull("fileData", fileData); 1167 try { 1168 final RandomAccessFile raf = new RandomAccessFile(destFile, "rw"); 1169 try { 1170 raf.seek(offset); 1171 raf.write(fileData); 1172 } finally { 1173 raf.close(); 1174 } 1175 logger.trace("writeFileDataToDestFile: Wrote {} bytes at offset {} to '{}'.", fileData.length, offset, destFile.getAbsolutePath()); 1176 } catch (IOException e) { 1177 throw new RuntimeException(e); 1178 } 1179 } 1180 1181 private void writeFileDataToTempChunkFile(final File destFile, final long offset, final byte[] fileData) { 1182 assertNotNull("destFile", destFile); 1183 assertNotNull("fileData", fileData); 1184 try { 1185 final File tempChunkFile = createTempChunkFile(destFile, offset); 1186 final File tempChunkFileDTOFile = getTempChunkFileDTOFile(tempChunkFile); 1187 1188 // Delete the meta-data-file, in case we overwrite an older temp-chunk-file. This way it 1189 // is guaranteed, that if the meta-data-file exists, it is consistent with either 1190 // the temp-chunk-file or the chunk was already written into the final destination. 1191 deleteOrFail(tempChunkFileDTOFile); 1192 1193 final FileOutputStream out = new FileOutputStream(tempChunkFile); 1194 try { 1195 out.write(fileData); 1196 } finally { 1197 out.close(); 1198 } 1199 final String sha1 = sha1(fileData); 1200 logger.trace("writeFileDataToTempChunkFile: Wrote {} bytes with SHA1 '{}' to '{}'.", fileData.length, sha1, tempChunkFile.getAbsolutePath()); 1201 final TempChunkFileDTO tempChunkFileDTO = createTempChunkFileDTO(offset, tempChunkFile, sha1); 1202 new TempChunkFileDTOIO().serialize(tempChunkFileDTO, tempChunkFileDTOFile); 1203 } catch (IOException e) { 1204 throw new RuntimeException(e); 1205 } 1206 } 1207 1208 private void deleteTempChunkFilesWithoutDTOFile(Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles) { 1209 for (final TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) { 1210 final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile(); 1211 if (tempChunkFileDTOFile == null || !tempChunkFileDTOFile.exists()) { 1212 File tempChunkFile = tempChunkFileWithDTOFile.getTempChunkFile(); 1213 logger.warn("deleteTempChunkFilesWithoutDTOFile: No DTO-file for temporary chunk-file '{}'! DELETING this temporary file!", tempChunkFile.getAbsolutePath()); 1214 deleteOrFail(tempChunkFile); 1215 continue; 1216 } 1217 } 1218 } 1219 1220 private Map<Long, TempChunkFileWithDTOFile> getOffset2TempChunkFileWithDTOFile(final File destFile) { 1221 final File[] tempFiles = getTempDir(destFile).listFiles(); 1222 if (tempFiles == null) 1223 return Collections.emptyMap(); 1224 1225 final String destFileName = destFile.getName(); 1226 final Map<Long, TempChunkFileWithDTOFile> result = new TreeMap<Long, TempChunkFileWithDTOFile>(); 1227 for (final File tempFile : tempFiles) { 1228 String tempFileName = tempFile.getName(); 1229 if (!tempFileName.startsWith(TEMP_CHUNK_FILE_PREFIX)) 1230 continue; 1231 1232 final boolean dtoFile; 1233 if (tempFileName.endsWith(TEMP_CHUNK_FILE_DTO_FILE_SUFFIX)) { 1234 dtoFile = true; 1235 tempFileName = tempFileName.substring(0, tempFileName.length() - TEMP_CHUNK_FILE_DTO_FILE_SUFFIX.length()); 1236 } 1237 else 1238 dtoFile = false; 1239 1240 final int lastUnderscoreIndex = tempFileName.lastIndexOf('_'); 1241 if (lastUnderscoreIndex < 0) 1242 throw new IllegalStateException("lastUnderscoreIndex < 0 :: tempFileName='" + tempFileName + '\''); 1243 1244 final String tempFileDestFileName = tempFileName.substring(TEMP_CHUNK_FILE_PREFIX.length(), lastUnderscoreIndex); 1245 if (!destFileName.equals(tempFileDestFileName)) 1246 continue; 1247 1248 final String offsetStr = tempFileName.substring(lastUnderscoreIndex + 1); 1249 final Long offset = Long.valueOf(offsetStr, 36); 1250 TempChunkFileWithDTOFile tempChunkFileWithDTOFile = result.get(offset); 1251 if (tempChunkFileWithDTOFile == null) { 1252 tempChunkFileWithDTOFile = new TempChunkFileWithDTOFile(); 1253 result.put(offset, tempChunkFileWithDTOFile); 1254 } 1255 if (dtoFile) 1256 tempChunkFileWithDTOFile.setTempChunkFileDTOFile(tempFile); 1257 else 1258 tempChunkFileWithDTOFile.setTempChunkFile(tempFile); 1259 } 1260 return Collections.unmodifiableMap(result); 1261 } 1262 1263 private File getTempChunkFileDTOFile(File file) { 1264 return new File(file.getParentFile(), file.getName() + TEMP_CHUNK_FILE_DTO_FILE_SUFFIX); 1265 } 1266 1267 private String sha1(byte[] data) { 1268 assertNotNull("data", data); 1269 try { 1270 byte[] hash = HashUtil.hash(HashUtil.HASH_ALGORITHM_SHA, new ByteArrayInputStream(data)); 1271 return HashUtil.encodeHexStr(hash); 1272 } catch (NoSuchAlgorithmException e) { 1273 throw new RuntimeException(e); 1274 } catch (IOException e) { 1275 throw new RuntimeException(e); 1276 } 1277 } 1278 1279 /** 1280 * Create the temporary file for the given {@code destFile} and {@code offset}. 1281 * <p> 1282 * The returned file is created, if it does not yet exist; but it is <i>not</i> overwritten, 1283 * if it already exists. 1284 * <p> 1285 * The {@linkplain #getTempDir(File) temporary directory} in which the temporary file is located 1286 * is created, if necessary. In order to prevent collisions with code trying to delete the empty 1287 * temporary directory, this method and the corresponding {@link #deleteTempDirIfEmpty(File)} are 1288 * both synchronized. 1289 * @param destFile the destination file for which to resolve and create the temporary file. 1290 * Must not be <code>null</code>. 1291 * @param offset the offset (inside the final destination file and the source file) of the block to 1292 * be temporarily stored in the temporary file created by this method. The temporary file will hold 1293 * solely this block (thus the offset in the temporary file is 0). 1294 * @return the temporary file. Never <code>null</code>. The file is already created in the file system 1295 * (empty), if it did not yet exist. 1296 */ 1297 private synchronized File createTempChunkFile(final File destFile, final long offset) { 1298 final File tempDir = getTempDir(destFile); 1299 tempDir.mkdir(); 1300 if (!tempDir.isDirectory()) 1301 throw new IllegalStateException("Creating the directory failed (it does not exist after mkdir): " + tempDir.getAbsolutePath()); 1302 1303 final File tempFile = new File(tempDir, String.format("%s%s_%s", 1304 TEMP_CHUNK_FILE_PREFIX, destFile.getName(), Long.toString(offset, 36))); 1305 try { 1306 tempFile.createNewFile(); 1307 } catch (IOException e) { 1308 throw new RuntimeException(e); 1309 } 1310 return tempFile; 1311 } 1312 1313 /** 1314 * Deletes the {@linkplain #getTempDir(File) temporary directory} for the given {@code destFile}, 1315 * if this directory is empty. 1316 * <p> 1317 * This method is synchronized to prevent it from colliding with {@link #createTempChunkFile(File, long)} 1318 * which first creates the temporary directory and then the file in it. Without synchronisation, the 1319 * newly created directory might be deleted by this method, before the temporary file in it is created. 1320 * @param destFile the destination file for which to resolve and delete the temporary directory. 1321 * Must not be <code>null</code>. 1322 */ 1323 private synchronized void deleteTempDirIfEmpty(final File destFile) { 1324 final File tempDir = getTempDir(destFile); 1325 tempDir.delete(); // deletes only empty directories ;-) 1326 } 1327 1328 private File getTempDir(final File destFile) { 1329 assertNotNull("destFile", destFile); 1330 final File parentDir = destFile.getParentFile(); 1331 return new File(parentDir, LocalRepoManager.TEMP_DIR_NAME); 1332 } 1333 1334 private final Map<File, FileWriteStrategy> file2FileWriteStrategy = new WeakHashMap<>(); 1335 1336 private FileWriteStrategy getFileWriteStrategy(File file) { 1337 assertNotNull("file", file); 1338 synchronized (file2FileWriteStrategy) { 1339 FileWriteStrategy fileWriteStrategy = file2FileWriteStrategy.get(file); 1340 if (fileWriteStrategy == null) { 1341 fileWriteStrategy = Config.getInstanceForFile(file).getPropertyAsEnum(FileWriteStrategy.CONFIG_KEY, FileWriteStrategy.CONFIG_DEFAULT_VALUE); 1342 file2FileWriteStrategy.put(file, fileWriteStrategy); 1343 } 1344 return fileWriteStrategy; 1345 } 1346 } 1347 1348 @Override 1349 public void endPutFile(String path, final Date lastModified, final long length, String sha1) { 1350 path = prefixPath(path); 1351 assertNotNull("lastModified", lastModified); 1352 assertNotNull("sha1", sha1); 1353 final File file = getFile(path); 1354 final File parentFile = file.getParentFile(); 1355 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 1356 final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 1357 try { 1358 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 1359 try { 1360 final RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), file); 1361 if (!(repoFile instanceof NormalFile)) { 1362 throw new IllegalStateException(String.format("RepoFile is not an instance of NormalFile! repoFile=%s file=%s", 1363 repoFile, file)); 1364 } 1365 1366 final NormalFile normalFile = (NormalFile) repoFile; 1367 if (!normalFile.isInProgress()) 1368 throw new IllegalStateException(String.format("NormalFile.inProgress == false! beginFile(...) not called?! repoFile=%s file=%s", 1369 repoFile, file)); 1370 1371 final FileWriteStrategy fileWriteStrategy = getFileWriteStrategy(file); 1372 logger.debug("endPutFile: fileWriteStrategy={}", fileWriteStrategy); 1373 1374 final File destFile = (fileWriteStrategy == FileWriteStrategy.replaceAfterTransfer 1375 ? new File(file.getParentFile(), LocalRepoManager.TEMP_NEW_FILE_PREFIX + file.getName()) : file); 1376 1377 final InputStream fileIn; 1378 if (destFile != file) { 1379 try { 1380 fileIn = new FileInputStream(file); 1381 destFile.createNewFile(); 1382 } catch (IOException e) { 1383 throw new RuntimeException(e); 1384 } 1385 } 1386 else 1387 fileIn = null; 1388 1389 final TempChunkFileDTOIO tempChunkFileDTOIO = new TempChunkFileDTOIO(); 1390 long destFileWriteOffset = 0; 1391 // tempChunkFileWithDTOFiles are sorted by offset (ascending) 1392 final Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles = getOffset2TempChunkFileWithDTOFile(file).values(); 1393 for (final TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) { 1394 final File tempChunkFile = tempChunkFileWithDTOFile.getTempChunkFile(); // tempChunkFile may be null!!! 1395 final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile(); 1396 if (tempChunkFileDTOFile == null) 1397 throw new IllegalStateException("No meta-data (tempChunkFileDTOFile) for file: " + (tempChunkFile == null ? null : tempChunkFile.getAbsolutePath())); 1398 1399 final TempChunkFileDTO tempChunkFileDTO = tempChunkFileDTOIO.deserialize(tempChunkFileDTOFile); 1400 final long offset = assertNotNull("tempChunkFileDTO.fileChunkDTO", tempChunkFileDTO.getFileChunkDTO()).getOffset(); 1401 1402 if (fileIn != null) { 1403 // The following might fail, if *file* was truncated during the transfer. In this case, 1404 // throwing an exception now is probably the best choice as the next sync run will 1405 // continue cleanly. 1406 writeFileDataToDestFile(destFile, destFileWriteOffset, fileIn, offset - destFileWriteOffset); 1407 final long tempChunkFileLength = tempChunkFileDTO.getFileChunkDTO().getLength(); 1408 skipOrFail(fileIn, tempChunkFileLength); // skipping beyond the EOF is supported by the FileInputStream according to Javadoc. 1409 destFileWriteOffset = offset + tempChunkFileLength; 1410 } 1411 1412 if (tempChunkFile != null && tempChunkFile.exists()) { 1413 writeTempChunkFileToDestFile(destFile, tempChunkFile, tempChunkFileDTO); 1414 deleteOrFail(tempChunkFile); 1415 } 1416 } 1417 1418 if (fileIn != null && destFileWriteOffset < length) 1419 writeFileDataToDestFile(destFile, destFileWriteOffset, fileIn, length - destFileWriteOffset); 1420 1421 try { 1422 final RandomAccessFile raf = new RandomAccessFile(destFile, "rw"); 1423 try { 1424 raf.setLength(length); 1425 } finally { 1426 raf.close(); 1427 } 1428 } catch (IOException e) { 1429 throw new RuntimeException(e); 1430 } 1431 1432 if (destFile != file) { 1433 deleteOrFail(file); 1434 destFile.renameTo(file); 1435 if (!file.exists()) 1436 throw new IllegalStateException(String.format("Renaming the file from '%s' to '%s' failed: The destination file does not exist.", destFile.getAbsolutePath(), file.getAbsolutePath())); 1437 1438 if (destFile.exists()) 1439 throw new IllegalStateException(String.format("Renaming the file from '%s' to '%s' failed: The source file still exists.", destFile.getAbsolutePath(), file.getAbsolutePath())); 1440 } 1441 1442 deleteTempChunkFiles(tempChunkFileWithDTOFiles); 1443 deleteTempDirIfEmpty(file); 1444 1445 LocalRepoSync localRepoSync = new LocalRepoSync(transaction); 1446 file.setLastModified(lastModified.getTime()); 1447 localRepoSync.updateRepoFile(normalFile, file, new NullProgressMonitor()); 1448 normalFile.setLastSyncFromRepositoryId(clientRepositoryId); 1449 normalFile.setInProgress(false); 1450 1451 logger.trace("endPutFile: Committing: sha1='{}' file='{}'", normalFile.getSha1(), file); 1452 if (!sha1.equals(normalFile.getSha1())) { 1453 logger.warn("endPutFile: File was modified during transport (either on source or destination side): expectedSha1='{}' foundSha1='{}' file='{}'", 1454 sha1, normalFile.getSha1(), file); 1455 } 1456 1457 } finally { 1458 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 1459 } 1460 transaction.commit(); 1461 } finally { 1462 transaction.rollbackIfActive(); 1463 } 1464 } 1465 1466 private void deleteTempChunkFiles(final Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles) { 1467 for (final TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) { 1468 final File tempChunkFile = tempChunkFileWithDTOFile.getTempChunkFile(); // tempChunkFile may be null!!! 1469 final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile(); 1470 1471 if (tempChunkFile != null && tempChunkFile.exists()) 1472 deleteOrFail(tempChunkFile); 1473 1474 if (tempChunkFileDTOFile != null && tempChunkFileDTOFile.exists()) 1475 deleteOrFail(tempChunkFileDTOFile); 1476 } 1477 } 1478 1479 private void deleteOrFail(File file) { 1480 file.delete(); 1481 if (isSymlink(file) || file.exists()) 1482 throw new IllegalStateException("Could not delete file (it still exists after deletion): " + file); 1483 } 1484 1485 /** 1486 * Skip the given {@code length} number of bytes. 1487 * <p> 1488 * Because {@link InputStream#skip(long)} and {@link FileInputStream#skip(long)} are both documented to skip 1489 * over less than the requested number of bytes "for a number of reasons", this method invokes the underlying 1490 * skip(...) method multiple times until either EOF is reached or the requested number of bytes was skipped. 1491 * In case of EOF, an 1492 * @param in the {@link InputStream} to be skipped. Must not be <code>null</code>. 1493 * @param length the number of bytes to be skipped. Must not be negative (i.e. <code>length >= 0</code>). 1494 */ 1495 private void skipOrFail(InputStream in, long length) { 1496 assertNotNull("in", in); 1497 if (length < 0) 1498 throw new IllegalArgumentException("length < 0"); 1499 1500 long skipped = 0; 1501 int skippedNowWas0Counter = 0; 1502 while (skipped < length) { 1503 final long toSkip = length - skipped; 1504 try { 1505 final long skippedNow = in.skip(toSkip); 1506 if (skippedNow < 0) 1507 throw new IOException("in.skip(" + toSkip + ") returned " + skippedNow); 1508 1509 if (skippedNow == 0) { 1510 if (++skippedNowWas0Counter >= 5) { 1511 throw new IOException(String.format( 1512 "Could not skip %s consecutive times!", skippedNowWas0Counter)); 1513 } 1514 } 1515 else 1516 skippedNowWas0Counter = 0; 1517 1518 skipped += skippedNow; 1519 } catch (IOException e) { 1520 throw new RuntimeException(e); 1521 } 1522 } 1523 } 1524 1525 private void writeFileDataToDestFile(final File destFile, final long offset, final InputStream in, final long length) { 1526 assertNotNull("destFile", destFile); 1527 assertNotNull("in", in); 1528 if (offset < 0) 1529 throw new IllegalArgumentException("offset < 0"); 1530 1531 if (length == 0) 1532 return; 1533 1534 if (length < 0) 1535 throw new IllegalArgumentException("length < 0"); 1536 1537 long lengthDone = 0; 1538 1539 try { 1540 final RandomAccessFile raf = new RandomAccessFile(destFile, "rw"); 1541 try { 1542 raf.seek(offset); 1543 1544 final byte[] buf = new byte[200 * 1024]; 1545 1546 while (lengthDone < length) { 1547 final long len = Math.min(length - lengthDone, buf.length); 1548 final int bytesRead = in.read(buf, 0, (int)len); 1549 if (bytesRead > 0) { 1550 raf.write(buf, 0, bytesRead); 1551 lengthDone += bytesRead; 1552 } 1553 else if (bytesRead < 0) 1554 throw new IOException("Premature end of stream!"); 1555 } 1556 } finally { 1557 raf.close(); 1558 } 1559 } catch (IOException e) { 1560 throw new RuntimeException(e); 1561 } 1562 } 1563 1564 @Override 1565 public void endSyncFromRepository() { 1566 UUID clientRepositoryId = getClientRepositoryIdOrFail(); 1567 LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 1568 try { 1569 PersistenceManager pm = ((co.codewizards.cloudstore.local.LocalRepoTransactionImpl)transaction).getPersistenceManager(); 1570 RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class); 1571 LastSyncToRemoteRepoDAO lastSyncToRemoteRepoDAO = transaction.getDAO(LastSyncToRemoteRepoDAO.class); 1572 ModificationDAO modificationDAO = transaction.getDAO(ModificationDAO.class); 1573 1574 RemoteRepository toRemoteRepository = remoteRepositoryDAO.getRemoteRepositoryOrFail(clientRepositoryId); 1575 1576 LastSyncToRemoteRepo lastSyncToRemoteRepo = lastSyncToRemoteRepoDAO.getLastSyncToRemoteRepoOrFail(toRemoteRepository); 1577 if (lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress() < 0) 1578 throw new IllegalStateException(String.format("lastSyncToRemoteRepo.localRepositoryRevisionInProgress < 0 :: There is no sync in progress for the RemoteRepository with entityID=%s", clientRepositoryId)); 1579 1580 lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress()); 1581 lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(-1); 1582 1583 pm.flush(); // prevent problems caused by batching, deletion and foreign keys 1584 Collection<Modification> modifications = modificationDAO.getModificationsBeforeOrEqual( 1585 toRemoteRepository, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()); 1586 modificationDAO.deletePersistentAll(modifications); 1587 pm.flush(); 1588 1589 transaction.commit(); 1590 } finally { 1591 transaction.rollbackIfActive(); 1592 } 1593 } 1594 1595 @Override 1596 public void endSyncToRepository(long fromLocalRevision) { 1597 UUID clientRepositoryId = getClientRepositoryIdOrFail(); 1598 LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 1599 try { 1600 RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class); 1601 RemoteRepository remoteRepository = remoteRepositoryDAO.getRemoteRepositoryOrFail(clientRepositoryId); 1602 remoteRepository.setRevision(fromLocalRevision); 1603 1604 transaction.commit(); 1605 } finally { 1606 transaction.rollbackIfActive(); 1607 } 1608 } 1609 1610}