001package co.codewizards.cloudstore.local.transport; 002 003import static co.codewizards.cloudstore.core.io.StreamUtil.*; 004import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; 005import static co.codewizards.cloudstore.core.util.AssertUtil.*; 006import static co.codewizards.cloudstore.core.util.IOUtil.*; 007 008import co.codewizards.cloudstore.core.io.ByteArrayInputStream; 009import java.io.FileInputStream; 010import java.io.IOException; 011import java.io.InputStream; 012import java.io.RandomAccessFile; 013import java.net.MalformedURLException; 014import java.net.URISyntaxException; 015import java.net.URL; 016import java.security.NoSuchAlgorithmException; 017import java.util.ArrayList; 018import java.util.Arrays; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.Comparator; 022import java.util.Date; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Properties; 027import java.util.Set; 028import java.util.UUID; 029import java.util.WeakHashMap; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 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.config.ConfigImpl; 040import co.codewizards.cloudstore.core.dto.ChangeSetDto; 041import co.codewizards.cloudstore.core.dto.ConfigPropSetDto; 042import co.codewizards.cloudstore.core.dto.DirectoryDto; 043import co.codewizards.cloudstore.core.dto.NormalFileDto; 044import co.codewizards.cloudstore.core.dto.RepoFileDto; 045import co.codewizards.cloudstore.core.dto.RepositoryDto; 046import co.codewizards.cloudstore.core.dto.SymlinkDto; 047import co.codewizards.cloudstore.core.dto.TempChunkFileDto; 048import co.codewizards.cloudstore.core.dto.VersionInfoDto; 049import co.codewizards.cloudstore.core.dto.jaxb.TempChunkFileDtoIo; 050import co.codewizards.cloudstore.core.oio.File; 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.CollisionException; 059import co.codewizards.cloudstore.core.repo.transport.DeleteModificationCollisionException; 060import co.codewizards.cloudstore.core.repo.transport.FileWriteStrategy; 061import co.codewizards.cloudstore.core.repo.transport.LocalRepoTransport; 062import co.codewizards.cloudstore.core.repo.transport.TransferDoneMarkerType; 063import co.codewizards.cloudstore.core.util.AssertUtil; 064import co.codewizards.cloudstore.core.util.HashUtil; 065import co.codewizards.cloudstore.core.util.IOUtil; 066import co.codewizards.cloudstore.core.util.PropertiesUtil; 067import co.codewizards.cloudstore.core.util.UrlUtil; 068import co.codewizards.cloudstore.core.version.VersionInfoProvider; 069import co.codewizards.cloudstore.local.FilenameFilterSkipMetaDir; 070import co.codewizards.cloudstore.local.LocalRepoSync; 071import co.codewizards.cloudstore.local.dto.RepoFileDtoConverter; 072import co.codewizards.cloudstore.local.dto.RepositoryDtoConverter; 073import co.codewizards.cloudstore.local.persistence.DeleteModification; 074import co.codewizards.cloudstore.local.persistence.DeleteModificationDao; 075import co.codewizards.cloudstore.local.persistence.Directory; 076import co.codewizards.cloudstore.local.persistence.FileInProgressMarker; 077import co.codewizards.cloudstore.local.persistence.FileInProgressMarkerDao; 078import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepo; 079import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepoDao; 080import co.codewizards.cloudstore.local.persistence.LocalRepository; 081import co.codewizards.cloudstore.local.persistence.LocalRepositoryDao; 082import co.codewizards.cloudstore.local.persistence.Modification; 083import co.codewizards.cloudstore.local.persistence.ModificationDao; 084import co.codewizards.cloudstore.local.persistence.NormalFile; 085import co.codewizards.cloudstore.local.persistence.RemoteRepository; 086import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDao; 087import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequest; 088import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequestDao; 089import co.codewizards.cloudstore.local.persistence.RepoFile; 090import co.codewizards.cloudstore.local.persistence.RepoFileDao; 091import co.codewizards.cloudstore.local.persistence.Symlink; 092import co.codewizards.cloudstore.local.persistence.TransferDoneMarker; 093import co.codewizards.cloudstore.local.persistence.TransferDoneMarkerDao; 094 095public class FileRepoTransport extends AbstractRepoTransport implements LocalRepoTransport { 096 private static final Logger logger = LoggerFactory.getLogger(FileRepoTransport.class); 097 098 private static final long MAX_REMOTE_REPOSITORY_REQUESTS_QUANTITY = 100; // TODO make configurable! 099 100 private LocalRepoManager localRepoManager; 101 private final TempChunkFileManager tempChunkFileManager = TempChunkFileManager.getInstance(); 102 103 @Override 104 public void close() { 105 if (localRepoManager != null) { 106 logger.debug("close: Closing localRepoManager."); 107 localRepoManager.close(); 108 } else 109 logger.debug("close: There is no localRepoManager."); 110 111 super.close(); 112 } 113 114 @Override 115 public UUID getRepositoryId() { 116 return getLocalRepoManager().getRepositoryId(); 117 } 118 119 @Override 120 public byte[] getPublicKey() { 121 return getLocalRepoManager().getPublicKey(); 122 } 123 124 @Override 125 public void requestRepoConnection(final byte[] publicKey) { 126 assertNotNull(publicKey, "publicKey"); 127 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 128 final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 129 try { 130 final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class); 131 final RemoteRepository remoteRepository = remoteRepositoryDao.getRemoteRepository(clientRepositoryId); 132 if (remoteRepository != null) 133 throw new IllegalArgumentException("RemoteRepository already connected! repositoryId=" + clientRepositoryId); 134 135 final String localPathPrefix = getPathPrefix(); 136 final RemoteRepositoryRequestDao remoteRepositoryRequestDao = transaction.getDao(RemoteRepositoryRequestDao.class); 137 RemoteRepositoryRequest remoteRepositoryRequest = remoteRepositoryRequestDao.getRemoteRepositoryRequest(clientRepositoryId); 138 if (remoteRepositoryRequest != null) { 139 logger.info("RemoteRepository already requested to be connected. repositoryId={}", clientRepositoryId); 140 141 // For security reasons, we do not allow to modify the public key! If we did, 142 // an attacker might replace the public key while the user is verifying the public key's 143 // fingerprint. The user would see & confirm the old public key, but the new public key 144 // would be written to the RemoteRepository. This requires really lucky timing, but 145 // if the attacker surveils the user, this might be feasable. 146 if (!Arrays.equals(remoteRepositoryRequest.getPublicKey(), publicKey)) 147 throw new IllegalStateException("Cannot modify the public key! Use 'dropRepoConnection' to drop the old request or wait until it expired."); 148 149 // For the same reasons stated above, we do not allow changing the local path-prefix, too. 150 if (!remoteRepositoryRequest.getLocalPathPrefix().equals(localPathPrefix)) 151 throw new IllegalStateException("Cannot modify the local path-prefix! Use 'dropRepoConnection' to drop the old request or wait until it expired."); 152 153 remoteRepositoryRequest.setChanged(new Date()); // make sure it is not deleted soon (the request expires after a while) 154 } 155 else { 156 final long remoteRepositoryRequestsCount = remoteRepositoryRequestDao.getObjectsCount(); 157 if (remoteRepositoryRequestsCount >= MAX_REMOTE_REPOSITORY_REQUESTS_QUANTITY) 158 throw new IllegalStateException(String.format( 159 "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)); 160 161 remoteRepositoryRequest = new RemoteRepositoryRequest(); 162 remoteRepositoryRequest.setRepositoryId(clientRepositoryId); 163 remoteRepositoryRequest.setPublicKey(publicKey); 164 remoteRepositoryRequest.setLocalPathPrefix(localPathPrefix); 165 remoteRepositoryRequestDao.makePersistent(remoteRepositoryRequest); 166 } 167 168 transaction.commit(); 169 } finally { 170 transaction.rollbackIfActive(); 171 } 172 } 173 174 @Override 175 public RepositoryDto getRepositoryDto() { 176 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction(); ) { 177 final LocalRepositoryDao localRepositoryDao = transaction.getDao(LocalRepositoryDao.class); 178 final LocalRepository localRepository = localRepositoryDao.getLocalRepositoryOrFail(); 179 final RepositoryDto repositoryDto = RepositoryDtoConverter.create().toRepositoryDto(localRepository); 180 transaction.commit(); 181 return repositoryDto; 182 } 183 } 184 185 @Override 186 public ChangeSetDto getChangeSetDto(final boolean localSync) { 187 if (localSync) 188 getLocalRepoManager().localSync(new LoggerProgressMonitor(logger)); 189 190 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 191 // We use a WRITE tx, because we write the LastSyncToRemoteRepo! 192 193 final ChangeSetDto changeSetDto = ChangeSetDtoBuilder 194 .create(transaction, this) 195 .buildChangeSetDto(); 196 197 transaction.commit(); 198 return changeSetDto; 199 } 200 } 201 202 @Override 203 public void prepareForChangeSetDto(ChangeSetDto changeSetDto) { 204 // nothing to do here. 205 } 206 207 @Override 208 public void makeDirectory(String path, final Date lastModified) { 209 path = prefixPath(path); 210 final File file = getFile(path); 211 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 212 final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); 213 try { 214 assertNoDeleteModificationCollision(transaction, clientRepositoryId, path); 215 mkDir(transaction, clientRepositoryId, file, lastModified); 216 transaction.commit(); 217 } finally { 218 transaction.rollbackIfActive(); 219 } 220 } 221 222 @Override 223 public void makeSymlink(String path, final String target, final Date lastModified) { 224 path = prefixPath(path); 225 AssertUtil.assertNotNull(target, "target"); 226 final File file = getFile(path); 227 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 228 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 229 final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class); 230 231 final File parentFile = file.getParentFile(); 232 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 233 try { 234 assertNoDeleteModificationCollision(transaction, clientRepositoryId, path); 235 236 if (file.existsNoFollow() && !file.isSymbolicLink()) 237 handleFileTypeCollision(transaction, clientRepositoryId, file, SymlinkDto.class); 238// file.renameTo(IOUtil.createCollisionFile(file)); 239 240 if (file.existsNoFollow() && !file.isSymbolicLink()) 241 throw new IllegalStateException("Could not rename file! It is still in the way: " + file); 242 243 final File localRoot = getLocalRepoManager().getLocalRoot(); 244 245 try { 246 final boolean currentTargetEqualsNewTarget; 247// final Path symlinkPath = file.toPath(); 248 if (file.isSymbolicLink()) { 249// final Path currentTargetPath = Files.readSymbolicLink(symlinkPath); 250 final String currentTarget = file.readSymbolicLinkToPathString(); 251 currentTargetEqualsNewTarget = currentTarget.equals(target); 252 if (!currentTargetEqualsNewTarget) { 253 final RepoFile repoFile = repoFileDao.getRepoFile(localRoot, file); 254 if (repoFile == null) // it's new - just created 255 handleFileCollision(transaction, clientRepositoryId, file); 256 else 257 detectAndHandleFileCollision(transaction, clientRepositoryId, parentFile, repoFile); 258 259 file.delete(); 260 } 261 } 262 else 263 currentTargetEqualsNewTarget = false; 264 265 if (!currentTargetEqualsNewTarget) 266 file.createSymbolicLink(target); 267 268 if (lastModified != null) 269 file.setLastModifiedNoFollow(lastModified.getTime()); 270 271 } catch (final IOException e) { 272 throw new RuntimeException(e); 273 } 274 275 final RepoFile repoFile = syncRepoFile(transaction, file); 276 277 if (repoFile == null) 278 throw new IllegalStateException("LocalRepoSync.sync(...) did not create the RepoFile for file: " + file); 279 280 if (!(repoFile instanceof Symlink)) 281 throw new IllegalStateException("LocalRepoSync.sync(...) created an instance of " + repoFile.getClass().getName() + " instead of a Symlink for file: " + file); 282 283 repoFile.setLastSyncFromRepositoryId(clientRepositoryId); 284 285 final Collection<TempChunkFileWithDtoFile> tempChunkFileWithDtoFiles = tempChunkFileManager.getOffset2TempChunkFileWithDtoFile(file).values(); 286 for (final TempChunkFileWithDtoFile tempChunkFileWithDtoFile : tempChunkFileWithDtoFiles) { 287 if (tempChunkFileWithDtoFile.getTempChunkFileDtoFile() != null) 288 deleteOrFail(tempChunkFileWithDtoFile.getTempChunkFileDtoFile()); 289 290 if (tempChunkFileWithDtoFile.getTempChunkFile() != null) 291 deleteOrFail(tempChunkFileWithDtoFile.getTempChunkFile()); 292 } 293 } catch (IOException x) { 294 throw new RuntimeException(x); 295 } finally { 296 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 297 } 298 299 transaction.commit(); 300 } 301 } 302 303 protected void assertNoDeleteModificationCollision(final LocalRepoTransaction transaction, final UUID fromRepositoryId, String path) throws CollisionException { 304 final RemoteRepository fromRemoteRepository = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryOrFail(fromRepositoryId); 305 final long lastSyncFromRemoteRepositoryLocalRevision = fromRemoteRepository.getLocalRevision(); 306 307 if (!path.startsWith("/")) 308 path = '/' + path; 309 310 final DeleteModificationDao deleteModificationDao = transaction.getDao(DeleteModificationDao.class); 311 final Collection<DeleteModification> deleteModifications = deleteModificationDao.getDeleteModificationsForPathOrParentOfPathAfter( 312 path, lastSyncFromRemoteRepositoryLocalRevision, fromRemoteRepository); 313 314 if (!deleteModifications.isEmpty()) 315 throw new DeleteModificationCollisionException( 316 String.format("There is at least one DeleteModification for repositoryId=%s path='%s'", fromRepositoryId, path)); 317 } 318 319 @Override 320 public void copy(String fromPath, String toPath) { 321 fromPath = prefixPath(fromPath); 322 toPath = prefixPath(toPath); 323 324 final File fromFile = getFile(fromPath); 325 final File toFile = getFile(toPath); 326 327 if (!fromFile.isFile()) // TODO throw an exception and catch in RepoToRepoSync! 328 return; 329 330 if (toFile.existsNoFollow()) // TODO either simply throw an exception or implement proper collision check. 331 return; 332 333 final File toParentFile = toFile.getParentFile(); 334 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 335 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(toParentFile); 336 try { 337 try { 338 if (!toParentFile.isDirectory()) 339 toParentFile.mkdirs(); 340 341 fromFile.copyToCopyAttributes(toFile); 342 } catch (final IOException e) { 343 throw new RuntimeException(e); 344 } 345 346 final LocalRepoSync localRepoSync = LocalRepoSync.create(transaction); 347 final RepoFile toRepoFile = localRepoSync.sync(toFile, new NullProgressMonitor(), true); 348 AssertUtil.assertNotNull(toRepoFile, "toRepoFile"); 349 toRepoFile.setLastSyncFromRepositoryId(getClientRepositoryIdOrFail()); 350 } finally { 351 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(toParentFile); 352 } 353 transaction.commit(); 354 } 355 } 356 357 @Override 358 public void move(String fromPath, String toPath) { 359 fromPath = prefixPath(fromPath); 360 toPath = prefixPath(toPath); 361 362 final File fromFile = getFile(fromPath); 363 final File toFile = getFile(toPath); 364 365 if (!fromFile.isFile()) // TODO throw an exception and catch in RepoToRepoSync! 366 return; 367 368 if (toFile.existsNoFollow()) // TODO either simply throw an exception or implement proper collision check. 369 return; 370 371 final File fromParentFile = fromFile.getParentFile(); 372 final File toParentFile = toFile.getParentFile(); 373 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 374 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(fromParentFile); 375 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(toParentFile); 376 try { 377 try { 378 if (!toParentFile.isDirectory()) 379 toParentFile.mkdirs(); 380 381 fromFile.move(toFile); 382 } catch (final IOException e) { 383 throw new RuntimeException(e); 384 } 385 386 final LocalRepoSync localRepoSync = LocalRepoSync.create(transaction); 387 final RepoFile toRepoFile = localRepoSync.sync(toFile, new NullProgressMonitor(), true); 388 final RepoFile fromRepoFile = transaction.getDao(RepoFileDao.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fromFile); 389 if (fromRepoFile != null) 390 localRepoSync.deleteRepoFile(fromRepoFile); 391 392 assertNotNull(toRepoFile, "toRepoFile"); 393 394 toRepoFile.setLastSyncFromRepositoryId(getClientRepositoryIdOrFail()); 395 } finally { 396 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(fromParentFile); 397 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(toParentFile); 398 } 399 transaction.commit(); 400 } 401 moveFileInProgressLocalRepo(getClientRepositoryId(), getRepositoryId(), fromPath, toPath); 402 tempChunkFileManager.moveChunks(fromFile, toFile); 403 } 404 405 private void moveFileInProgressLocalRepo(final UUID fromRepositoryId, final UUID toRepositoryId, 406 String fromPath, String toPath) { 407 fromPath = prefixPath(fromPath); 408 toPath = prefixPath(toPath); 409 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 410 final FileInProgressMarkerDao fileInProgressMarkerDao = transaction.getDao(FileInProgressMarkerDao.class); 411 final FileInProgressMarker toFileInProgressMarker = fileInProgressMarkerDao.getFileInProgressMarker(fromRepositoryId, toRepositoryId, fromPath); 412 if (toFileInProgressMarker != null ) { 413 logger.info("Updating FileInProgressMarker: {}, new toPath={}", toFileInProgressMarker, toPath); 414 toFileInProgressMarker.setPath(toPath); 415 } 416 transaction.commit(); 417 } 418 } 419 420 @Override 421 public void delete(String path) { 422 path = prefixPath(path); 423 final File file = getFile(path); 424 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 425 final boolean fileIsLocalRoot = getLocalRepoManager().getLocalRoot().equals(file); 426 final File parentFile = file.getParentFile(); 427 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 428 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 429 try { 430 final LocalRepoSync localRepoSync = LocalRepoSync.create(transaction); // not sure about the ignoreRulesEnabled here. 431 localRepoSync.sync(file, new NullProgressMonitor(), true); 432 433 if (fileIsLocalRoot) { 434 // Cannot delete the repository's root! Deleting all its contents instead. 435 final long fileLastModified = file.lastModified(); 436 try { 437 final File[] children = file.listFiles(new FilenameFilterSkipMetaDir()); 438 if (children == null) 439 throw new IllegalStateException("File-listing localRoot returned null: " + file); 440 441 for (final File child : children) 442 delete(transaction, localRepoSync, clientRepositoryId, child); 443 } finally { 444 file.setLastModified(fileLastModified); 445 } 446 } 447 else 448 delete(transaction, localRepoSync, clientRepositoryId, file); 449 450 } finally { 451 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 452 } 453 transaction.commit(); 454 } 455 } 456 457 private void delete(final LocalRepoTransaction transaction, final LocalRepoSync localRepoSync, final UUID fromRepositoryId, final File file) { 458 if (detectFileCollisionRecursively(transaction, fromRepositoryId, file)) 459 handleFileCollision(transaction, fromRepositoryId, file); 460 461 if (!IOUtil.deleteDirectoryRecursively(file)) { 462 throw new IllegalStateException("Deleting file or directory failed: " + file); 463 } 464 465 final RepoFile repoFile = transaction.getDao(RepoFileDao.class).getRepoFile(getLocalRepoManager().getLocalRoot(), file); 466 if (repoFile != null) 467 localRepoSync.deleteRepoFile(repoFile); 468 } 469 470 @Override 471 public RepoFileDto getRepoFileDto(String path) { 472 RepoFileDto repoFileDto = null; 473 path = prefixPath(path); 474 final File file = getFile(path); 475 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 476 // WRITE tx, because it performs a local sync! 477 478 final LocalRepoSync localRepoSync = LocalRepoSync.create(transaction); 479 localRepoSync.sync(file, new NullProgressMonitor(), false); // TODO or do we need recursiveChildren==true here? 480 481 final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class); 482 final RepoFile repoFile = repoFileDao.getRepoFile(getLocalRepoManager().getLocalRoot(), file); 483 if (repoFile != null) { 484 final RepoFileDtoConverter converter = RepoFileDtoConverter.create(transaction); 485 repoFileDto = converter.toRepoFileDto(repoFile, Integer.MAX_VALUE); // TODO pass depth as argument - or maybe leave it this way? 486 } 487 488 transaction.commit(); 489 } catch (final RuntimeException x) { 490 throw x; 491 } catch (final Exception x) { 492 throw new RuntimeException(x); 493 } 494 return repoFileDto; 495 } 496 497 @Override 498 public LocalRepoManager getLocalRepoManager() { 499 if (localRepoManager == null) { 500 logger.debug("getLocalRepoManager: Creating a new LocalRepoManager."); 501 File remoteRootFile; 502 try { 503 remoteRootFile = createFile(getRemoteRootWithoutPathPrefix().toURI()); 504 } catch (final URISyntaxException e) { 505 throw new RuntimeException(e); 506 } 507 localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(remoteRootFile); 508 } 509 return localRepoManager; 510 } 511 512 @Override 513 protected URL determineRemoteRootWithoutPathPrefix() { 514 final File remoteRootFile = UrlUtil.getFile(getRemoteRoot()); 515 516 final File localRootFile = LocalRepoHelper.getLocalRootContainingFile(remoteRootFile); 517 if (localRootFile == null) 518 throw new IllegalStateException(String.format( 519 "remoteRoot='%s' does not point to a file or directory within an existing repository (nor its root directory)!", 520 getRemoteRoot())); 521 522 try { 523 return localRootFile.toURI().toURL(); 524 } catch (final MalformedURLException e) { 525 throw new RuntimeException(e); 526 } 527 } 528 529// private List<FileChunkDto> toFileChunkDtos(final Set<FileChunk> fileChunks) { 530// final long startTimestamp = System.currentTimeMillis(); 531// final List<FileChunkDto> result = new ArrayList<FileChunkDto>(AssertUtil.assertNotNull("fileChunks", fileChunks).size()); 532// for (final FileChunk fileChunk : fileChunks) { 533// final FileChunkDto fileChunkDto = toFileChunkDto(fileChunk); 534// if (fileChunkDto != null) 535// result.add(fileChunkDto); 536// } 537// logger.debug("toFileChunkDtos: Creating {} FileChunkDtos took {} ms.", result.size(), System.currentTimeMillis() - startTimestamp); 538// return result; 539// } 540// 541// private FileChunkDto toFileChunkDto(final FileChunk fileChunk) { 542// final FileChunkDto dto = new FileChunkDto(); 543// dto.setLength(fileChunk.getLength()); 544// dto.setOffset(fileChunk.getOffset()); 545// dto.setSha1(fileChunk.getSha1()); 546// return dto; 547// } 548// private List<RepoFileDto> toRepoFileDtos(final Collection<RepoFile> fileChunks) { 549// final long startTimestamp = System.currentTimeMillis(); 550// final RepoFileDtoConverter converter = new RepoFileDtoConverter(transaction); 551// final List<RepoFileDto> result = new ArrayList<RepoFileDto>(AssertUtil.assertNotNull("fileChunks", fileChunks).size()); 552// for (final RepoFile fileChunk : fileChunks) { 553// final RepoFileDto fileChunkDto = toRepoFileDto(fileChunk); 554// if (fileChunkDto != null) 555// result.add(fileChunkDto); 556// } 557// logger.debug("toFileChunkDtos: Creating {} FileChunkDtos took {} ms.", result.size(), System.currentTimeMillis() - startTimestamp); 558// return result; 559// } 560// 561// private RepoFileDto toRepoFileDto(final RepoFile repoFile) { 562// final FileChunkDto dto = new FileChunkDto(); 563// dto.setLength(repoFile.getLength()); 564// dto.setOffset(repoFile.getOffset()); 565// dto.setSha1(repoFile.getSha1()); 566// return dto; 567// } 568 569 570 protected void mkDir(final LocalRepoTransaction transaction, final UUID clientRepositoryId, final File file, final Date lastModified) { 571 AssertUtil.assertNotNull(transaction, "transaction"); 572 AssertUtil.assertNotNull(file, "file"); 573 574 final File localRoot = getLocalRepoManager().getLocalRoot(); 575 final File parentFile = localRoot.equals(file) ? null : file.getParentFile(); 576 577 if (parentFile != null) 578 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 579 580 try { 581 RepoFile parentRepoFile = parentFile == null ? null : transaction.getDao(RepoFileDao.class).getRepoFile(localRoot, parentFile); 582 583 if (parentFile != null) { 584 if (!localRoot.equals(parentFile) && (!parentFile.isDirectory() || parentRepoFile == null)) 585 mkDir(transaction, clientRepositoryId, parentFile, null); 586 587 if (parentRepoFile == null) 588 parentRepoFile = transaction.getDao(RepoFileDao.class).getRepoFile(localRoot, parentFile); 589 590 if (parentRepoFile == null) // now, it should definitely not be null anymore! 591 throw new IllegalStateException("parentRepoFile == null"); 592 } 593 594 if (file.existsNoFollow() && !file.isDirectory()) 595 handleFileTypeCollision(transaction, clientRepositoryId, file, DirectoryDto.class); 596 597 if (file.existsNoFollow() && !file.isDirectory()) 598 throw new IllegalStateException("Could not rename file! It is still in the way: " + file); 599 600 if (!file.isDirectory()) 601 file.mkdir(); 602 603 if (!file.isDirectory()) 604 throw new IllegalStateException("Could not create directory (permissions?!): " + file); 605 606// RepoFile repoFile = transaction.getDao(RepoFileDao.class).getRepoFile(localRoot, file); 607// if (repoFile != null && !(repoFile instanceof Directory)) { 608// transaction.getDao(RepoFileDao.class).deletePersistent(repoFile); 609// repoFile = null; 610// } 611 612 if (lastModified != null) 613 file.setLastModified(lastModified.getTime()); 614 615 RepoFile repoFile = syncRepoFile(transaction, file); 616 if (repoFile == null) 617 throw new IllegalStateException("Just created directory, but corresponding RepoFile still does not exist after local sync: " + file); 618 619 if (!(repoFile instanceof Directory)) 620 throw new IllegalStateException("Just created directory, and even though the corresponding RepoFile now exists, it is not an instance of Directory! It is a " + repoFile.getClass().getName() + " instead! " + file); 621 622 repoFile.setLastSyncFromRepositoryId(clientRepositoryId); 623 } finally { 624 if (parentFile != null) 625 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 626 } 627 } 628 629 /** 630 * Syncs the single file/directory/symlink passed as {@code file} into the database non-recursively. 631 * @param transaction the current transaction. Must not be <code>null</code>. 632 * @param file the file (every type, i.e. might be a directory or symlink, too) to be synced. 633 * @return the {@link RepoFile} that was created/updated for the given {@code file}. 634 */ 635 protected RepoFile syncRepoFile(final LocalRepoTransaction transaction, final File file) { 636 assertNotNull(transaction, "transaction"); 637 assertNotNull(file, "file"); 638 return LocalRepoSync.create(transaction) 639 .sync(file, new NullProgressMonitor(), false); // recursiveChildren==false, because we only need this one single Directory object in the DB, and we MUST NOT consume time with its children. 640 } 641 642 /** 643 * @param path the prefixed path (relative to the real root). 644 * @return the file in the local repository. Never <code>null</code>. 645 */ 646 protected File getFile(String path) { 647 path = AssertUtil.assertNotNull(path, "path").replace('/', FILE_SEPARATOR_CHAR); 648 final File file = createFile(getLocalRepoManager().getLocalRoot(), path); 649 return file; 650 } 651 652 @Override 653 public byte[] getFileData(String path, final long offset, int length) { 654 path = prefixPath(path); 655 final File file = getFile(path); 656 try { 657 final RandomAccessFile raf = file.createRandomAccessFile("r"); 658 try { 659 raf.seek(offset); 660 if (length < 0) { 661 final long l = raf.length() - offset; 662 if (l > Integer.MAX_VALUE) 663 throw new IllegalArgumentException( 664 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.", 665 path, offset, length, Integer.MAX_VALUE)); 666 667 length = (int) l; 668 } 669 670 final byte[] bytes = new byte[length]; 671 int off = 0; 672 int numRead = 0; 673 while (off < bytes.length && (numRead = raf.read(bytes, off, bytes.length-off)) >= 0) { 674 off += numRead; 675 } 676 677 if (off < bytes.length) // Read INCOMPLETELY => discarding 678 return null; 679 680 return bytes; 681 } finally { 682 raf.close(); 683 } 684 } catch (final IOException e) { 685 throw new RuntimeException(e); 686 } 687 } 688 689 @Override 690 public void beginPutFile(String path) { 691 path = prefixPath(path); 692 final File file = getFile(path); // null-check already inside getFile(...) - no need for another check here 693 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 694 final File parentFile = file.getParentFile(); 695 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 696 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 697 try { 698 if (file.isSymbolicLink() || (file.exists() && !file.isFile())) // exists() and isFile() both resolve symlinks! Their result depends on where the symlink points to. 699 handleFileTypeCollision(transaction, clientRepositoryId, file, NormalFileDto.class); 700 701 if (file.isSymbolicLink() || (file.exists() && !file.isFile())) // the default implementation of handleFileTypeCollision(...) moves the file away. 702 throw new IllegalStateException("Could not rename file! It is still in the way: " + file); 703 704 final File localRoot = getLocalRepoManager().getLocalRoot(); 705 assertNoDeleteModificationCollision(transaction, clientRepositoryId, path); 706 707 boolean newFile = false; 708 if (!file.isFile()) { 709 newFile = true; 710 try { 711 file.createNewFile(); 712 } catch (final IOException e) { 713 throw new RuntimeException(e); 714 } 715 } 716 717 if (!file.isFile()) 718 throw new IllegalStateException("Could not create file (permissions?!): " + file); 719 720 // A complete sync run might take very long. Therefore, we better update our local meta-data 721 // *immediately* before beginning the sync of this file and before detecting a collision. 722 // Furthermore, maybe the file is new and there's no meta-data, yet, hence we must do this anyway. 723// final RepoFileDao repoFileDao = transaction.getDao(RepoFileDao.class); 724// LocalRepoSync.create(transaction).sync(file, new NullProgressMonitor(), false); // recursiveChildren has no effect on simple files, anyway (it's no directory). 725 726 tempChunkFileManager.deleteTempChunkFilesWithoutDtoFile(tempChunkFileManager.getOffset2TempChunkFileWithDtoFile(file).values()); 727 728 final RepoFile repoFile = syncRepoFile(transaction, file); 729 if (repoFile == null) 730 throw new IllegalStateException("LocalRepoSync.sync(...) did not create the RepoFile for file: " + file); 731 732 if (!(repoFile instanceof NormalFile)) 733 throw new IllegalStateException("LocalRepoSync.sync(...) created an instance of " + repoFile.getClass().getName() + " instead of a NormalFile for file: " + file); 734 735 final NormalFile normalFile = (NormalFile) repoFile; 736 737 if (!newFile && !normalFile.isInProgress()) 738 detectAndHandleFileCollision(transaction, clientRepositoryId, file, normalFile); 739 740 normalFile.setLastSyncFromRepositoryId(clientRepositoryId); 741 normalFile.setInProgress(true); 742 } finally { 743 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 744 } 745 transaction.commit(); 746 } 747 } 748 749 /** 750 * Handle a file-type-collision, which was already detected. 751 * <p> 752 * This method does not analyse whether there is a collision - this is already sure. 753 * It only handles the collision by logging and delegating to {@link #handleFileCollision(LocalRepoTransaction, UUID, File)}. 754 * @param transaction the DB transaction. Must not be <code>null</code>. 755 * @param fromRepositoryId the ID of the source repository from which the file is about to be copied. Must not be <code>null</code>. 756 * @param file the file that is to be copied (i.e. overwritten). Must not be <code>null</code>. This may be a directory or a symlink, too! 757 */ 758 protected void handleFileTypeCollision(final LocalRepoTransaction transaction, final UUID fromRepositoryId, final File file, final Class<? extends RepoFileDto> fromFileType) { 759 assertNotNull(transaction, "transaction"); 760 assertNotNull(fromRepositoryId, "fromRepositoryId"); 761 assertNotNull(file, "file"); 762 assertNotNull(fromFileType, "fromFileType"); 763 764 Class<? extends RepoFileDto> toFileType; 765 if (file.isSymbolicLink()) 766 toFileType = SymlinkDto.class; 767 else if (file.isFile()) 768 toFileType = NormalFileDto.class; 769 else if (file.isDirectory()) 770 toFileType = DirectoryDto.class; 771 else 772 throw new IllegalStateException("file has unknown type: " + file); 773 774 logger.info("handleFileTypeCollision: Collision: Destination file already exists, is modified and has a different type! toFileType={} fromFileType={} file='{}'", 775 toFileType.getSimpleName(), fromFileType.getSimpleName(), file.getAbsolutePath()); 776 777 final File collisionFile = handleFileCollision(transaction, fromRepositoryId, file); 778 LocalRepoSync.create(transaction).sync(collisionFile, new NullProgressMonitor(), true); // recursiveChildren==true, because the colliding thing might be a directory. 779 } 780 781 /** 782 * Detect if the file to be copied has been modified locally (or copied from another repository) after the last 783 * sync from the repository identified by {@code fromRepositoryId}. 784 * <p> 785 * If there is a collision - i.e. the destination file has been modified, too - then the destination file is moved 786 * away by renaming it. The name to which it is renamed is created by {@link IOUtil#createCollisionFile(File)}. 787 * Afterwards the file is copied back to its original name. 788 * <p> 789 * The reason for renaming it first (instead of directly copying it) is that there might be open file handles. 790 * In GNU/Linux, the open file handles stay open and thus are then connected to the renamed file, thus continuing 791 * to modify the file which was moved away. In Windows, the renaming likely fails and we abort with an exception. 792 * In both cases, we do our best to avoid both processes from writing to the same file simultaneously without locking 793 * it. 794 * <p> 795 * In the future (this is NOT YET IMPLEMENTED), we might lock it in {@link #beginPutFile(String)} and 796 * keep the lock until {@link #endPutFile(String, Date, long, String)} or a timeout occurs - and refresh the lock 797 * (i.e. postpone the timeout) with every {@link #putFileData(String, long, byte[])}. The reason for this 798 * quite complicated strategy is that we cannot guarantee that the {@link #endPutFile(String, Date, long, String)} 799 * is ever invoked (the client might crash inbetween). We don't want a locked file to linger forever. 800 * 801 * @param transaction the DB transaction. Must not be <code>null</code>. 802 * @param fromRepositoryId the ID of the source repository from which the file is about to be copied. Must not be <code>null</code>. 803 * @param file the file that is to be copied (i.e. overwritten). Must not be <code>null</code>. 804 * @param normalFileOrSymlink the DB entity corresponding to {@code file}. Must not be <code>null</code>. 805 */ 806 protected void detectAndHandleFileCollision(final LocalRepoTransaction transaction, final UUID fromRepositoryId, final File file, final RepoFile normalFileOrSymlink) { 807 assertNotNull(transaction, "transaction"); 808 assertNotNull(fromRepositoryId, "fromRepositoryId"); 809 assertNotNull(file, "file"); 810 assertNotNull(normalFileOrSymlink, "normalFileOrSymlink"); 811 if (detectFileCollision(transaction, fromRepositoryId, file, normalFileOrSymlink)) { 812 final File collisionFile = handleFileCollision(transaction, fromRepositoryId, file); 813 814 try { 815 collisionFile.copyToCopyAttributes(file); 816 } catch (final IOException e) { 817 throw new RuntimeException(e); 818 } 819 820 LocalRepoSync.create(transaction).sync(collisionFile, new NullProgressMonitor(), true); // TODO sub-progress-monitor! 821 } 822 } 823 824 protected File handleFileCollision(final LocalRepoTransaction transaction, final UUID fromRepositoryId, final File file) { 825 assertNotNull(transaction, "transaction"); 826 assertNotNull(fromRepositoryId, "fromRepositoryId"); 827 assertNotNull(file, "file"); 828 final File collisionFile = IOUtil.createCollisionFile(file); 829 file.renameTo(collisionFile); 830 if (file.existsNoFollow()) 831 throw new IllegalStateException("Could not rename file to resolve collision: " + file); 832 833 return collisionFile; 834 } 835 836 protected boolean detectFileCollisionRecursively(final LocalRepoTransaction transaction, final UUID fromRepositoryId, final File fileOrDirectory) { 837 AssertUtil.assertNotNull(transaction, "transaction"); 838 AssertUtil.assertNotNull(fromRepositoryId, "fromRepositoryId"); 839 AssertUtil.assertNotNull(fileOrDirectory, "fileOrDirectory"); 840 841 // we handle symlinks before invoking exists() below, because this method and most other File methods resolve symlinks! 842 if (fileOrDirectory.isSymbolicLink()) { 843 final RepoFile repoFile = transaction.getDao(RepoFileDao.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fileOrDirectory); 844 if (!(repoFile instanceof Symlink)) 845 return true; // We had a change after the last local sync (symlink => directory or normal file)! 846 847 return detectFileCollision(transaction, fromRepositoryId, fileOrDirectory, repoFile); 848 } 849 850 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?! 851 return false; 852 } 853 854 if (fileOrDirectory.isFile()) { 855 final RepoFile repoFile = transaction.getDao(RepoFileDao.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fileOrDirectory); 856 if (!(repoFile instanceof NormalFile)) 857 return true; // We had a change after the last local sync (normal file => directory or symlink)! 858 859 return detectFileCollision(transaction, fromRepositoryId, fileOrDirectory, repoFile); 860 } 861 862 final File[] children = fileOrDirectory.listFiles(); 863 if (children == null) 864 throw new IllegalStateException("listFiles() of directory returned null: " + fileOrDirectory); 865 866 for (final File child : children) { 867 if (detectFileCollisionRecursively(transaction, fromRepositoryId, child)) 868 return true; 869 } 870 871 return false; 872 } 873 874 /** 875 * Detect if the file to be copied or deleted has been modified locally (or copied from another repository) after the last 876 * sync from the repository identified by {@code fromRepositoryId}. 877 * @param transaction 878 * @param fromRepositoryId 879 * @param file 880 * @param normalFileOrSymlink 881 * @return <code>true</code>, if there is a collision; <code>false</code>, if there is none. 882 */ 883 protected boolean detectFileCollision(final LocalRepoTransaction transaction, final UUID fromRepositoryId, final File file, final RepoFile normalFileOrSymlink) { 884 AssertUtil.assertNotNull(transaction, "transaction"); 885 AssertUtil.assertNotNull(fromRepositoryId, "fromRepositoryId"); 886 AssertUtil.assertNotNull(file, "file"); 887 AssertUtil.assertNotNull(normalFileOrSymlink, "normalFileOrSymlink"); 888 889 if (!file.existsNoFollow()) { 890 logger.debug("detectFileCollision: path='{}': return false, because destination file does not exist.", normalFileOrSymlink.getPath()); 891 return false; 892 } 893 894 final RemoteRepository fromRemoteRepository = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryOrFail(fromRepositoryId); 895 final long lastSyncFromRemoteRepositoryLocalRevision = fromRemoteRepository.getLocalRevision(); 896 if (normalFileOrSymlink.getLocalRevision() <= lastSyncFromRemoteRepositoryLocalRevision) { 897 logger.debug("detectFileCollision: path='{}': return false, because: normalFileOrSymlink.localRevision <= lastSyncFromRemoteRepositoryLocalRevision :: {} <= {}", normalFileOrSymlink.getPath(), normalFileOrSymlink.getLocalRevision(), lastSyncFromRemoteRepositoryLocalRevision); 898 return false; 899 } 900 901 // The file was transferred from the same repository before and was thus not changed locally nor in another repo. 902 // This can only happen, if the sync was interrupted (otherwise the check for the localRevision above 903 // would have already caused this method to abort). 904 if (fromRepositoryId.equals(normalFileOrSymlink.getLastSyncFromRepositoryId())) { 905 logger.debug("detectFileCollision: path='{}': return false, because: fromRepositoryId == normalFileOrSymlink.lastSyncFromRepositoryId :: fromRepositoryId='{}'", normalFileOrSymlink.getPath(), fromRemoteRepository); 906 return false; 907 } 908 909 logger.debug("detectFileCollision: path='{}': return true! fromRepositoryId='{}' normalFileOrSymlink.localRevision={} lastSyncFromRemoteRepositoryLocalRevision={} normalFileOrSymlink.lastSyncFromRepositoryId='{}'", 910 normalFileOrSymlink.getPath(), fromRemoteRepository, normalFileOrSymlink.getLocalRevision(), lastSyncFromRemoteRepositoryLocalRevision, normalFileOrSymlink.getLastSyncFromRepositoryId()); 911 return true; 912 } 913 914 @Override 915 public void putFileData(String path, final long offset, final byte[] fileData) { 916 path = prefixPath(path); 917 final File file = getFile(path); 918 final File parentFile = file.getParentFile(); 919 final File localRoot = getLocalRepoManager().getLocalRoot(); 920 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction(); ) { 921 // READ tx: It writes into the file system, but it only reads from the DB. 922 923 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 924 try { 925 final RepoFile repoFile = transaction.getDao(RepoFileDao.class).getRepoFile(localRoot, file); 926 if (repoFile == null) 927 throw new IllegalStateException("No RepoFile found for file: " + file); 928 929 if (!(repoFile instanceof NormalFile)) 930 throw new IllegalStateException("RepoFile is not an instance of NormalFile for file: " + file); 931 932 final NormalFile normalFile = (NormalFile) repoFile; 933 if (!normalFile.isInProgress()) 934 throw new IllegalStateException(String.format("NormalFile.inProgress == false! beginPutFile(...) not called?! repoFile=%s file=%s", 935 repoFile, file)); 936 937 final FileWriteStrategy fileWriteStrategy = getFileWriteStrategy(file); 938 logger.debug("putFileData: fileWriteStrategy={}", fileWriteStrategy); 939 switch (fileWriteStrategy) { 940 case directDuringTransfer: 941 writeFileDataToDestFile(file, offset, fileData); 942 break; 943 case directAfterTransfer: 944 case replaceAfterTransfer: 945 tempChunkFileManager.writeFileDataToTempChunkFile(file, offset, fileData); 946 break; 947 default: 948 throw new IllegalStateException("Unknown fileWriteStrategy: " + fileWriteStrategy); 949 } 950 } finally { 951 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 952 } 953 transaction.commit(); 954 } 955 } 956 957 private void writeTempChunkFileToDestFile(final File destFile, final File tempChunkFile, final TempChunkFileDto tempChunkFileDto) { 958 AssertUtil.assertNotNull(destFile, "destFile"); 959 AssertUtil.assertNotNull(tempChunkFile, "tempChunkFile"); 960 AssertUtil.assertNotNull(tempChunkFileDto, "tempChunkFileDto"); 961 final long offset = AssertUtil.assertNotNull(tempChunkFileDto.getFileChunkDto(), "tempChunkFileDto.fileChunkDto").getOffset(); 962 final byte[] fileData = new byte[(int) tempChunkFile.length()]; 963 try { 964 final InputStream in = castStream(tempChunkFile.createInputStream()); 965 try { 966 int off = 0; 967 while (off < fileData.length) { 968 final int bytesRead = in.read(fileData, off, fileData.length - off); 969 if (bytesRead > 0) { 970 off += bytesRead; 971 } 972 else if (bytesRead < 0) { 973 throw new IllegalStateException("InputStream ended before expected file length!"); 974 } 975 } 976 if (off > fileData.length || in.read() != -1) 977 throw new IllegalStateException("InputStream contained more data than expected file length!"); 978 } finally { 979 in.close(); 980 } 981 } catch (final IOException e) { 982 throw new RuntimeException(e); 983 } 984 985 final String sha1FromDtoFile = tempChunkFileDto.getFileChunkDto().getSha1(); 986 final String sha1FromFileData = sha1(fileData); 987 988 logger.trace("writeTempChunkFileToDestFile: Read {} bytes with SHA1 '{}' from '{}'.", fileData.length, sha1FromFileData, tempChunkFile.getAbsolutePath()); 989 990 if (!sha1FromFileData.equals(sha1FromDtoFile)) 991 throw new IllegalStateException("SHA1 mismatch! Corrupt temporary chunk file or corresponding Dto file: " + tempChunkFile.getAbsolutePath()); 992 993 writeFileDataToDestFile(destFile, offset, fileData); 994 } 995 996 private void writeFileDataToDestFile(final File destFile, final long offset, final byte[] fileData) { 997 AssertUtil.assertNotNull(destFile, "destFile"); 998 AssertUtil.assertNotNull(fileData, "fileData"); 999 try { 1000 final RandomAccessFile raf = destFile.createRandomAccessFile("rw"); 1001 try { 1002 raf.seek(offset); 1003 raf.write(fileData); 1004 } finally { 1005 raf.close(); 1006 } 1007 logger.trace("writeFileDataToDestFile: Wrote {} bytes at offset {} to '{}'.", fileData.length, offset, destFile.getAbsolutePath()); 1008 } catch (final IOException e) { 1009 throw new RuntimeException(e); 1010 } 1011 } 1012 1013 private String sha1(final byte[] data) { 1014 AssertUtil.assertNotNull(data, "data"); 1015 try { 1016 final byte[] hash = HashUtil.hash(HashUtil.HASH_ALGORITHM_SHA, new ByteArrayInputStream(data)); 1017 return HashUtil.encodeHexStr(hash); 1018 } catch (final NoSuchAlgorithmException e) { 1019 throw new RuntimeException(e); 1020 } catch (final IOException e) { 1021 throw new RuntimeException(e); 1022 } 1023 } 1024 1025 private final Map<File, FileWriteStrategy> file2FileWriteStrategy = new WeakHashMap<>(); 1026 1027 private FileWriteStrategy getFileWriteStrategy(final File file) { 1028 AssertUtil.assertNotNull(file, "file"); 1029 synchronized (file2FileWriteStrategy) { 1030 FileWriteStrategy fileWriteStrategy = file2FileWriteStrategy.get(file); 1031 if (fileWriteStrategy == null) { 1032 fileWriteStrategy = ConfigImpl.getInstanceForFile(file).getPropertyAsEnum(FileWriteStrategy.CONFIG_KEY, FileWriteStrategy.CONFIG_DEFAULT_VALUE); 1033 file2FileWriteStrategy.put(file, fileWriteStrategy); 1034 } 1035 return fileWriteStrategy; 1036 } 1037 } 1038 1039 @Override 1040 public void endPutFile(String path, final Date lastModified, final long length, final String sha1) { 1041 path = prefixPath(path); 1042 AssertUtil.assertNotNull(lastModified, "lastModified"); 1043 final File file = getFile(path); 1044 final File parentFile = file.getParentFile(); 1045 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 1046 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 1047 ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile); 1048 try { 1049 final RepoFile repoFile = transaction.getDao(RepoFileDao.class).getRepoFile(getLocalRepoManager().getLocalRoot(), file); 1050 if (!(repoFile instanceof NormalFile)) { 1051 throw new IllegalStateException(String.format("RepoFile is not an instance of NormalFile! repoFile=%s file=%s", 1052 repoFile, file)); 1053 } 1054 1055 final NormalFile normalFile = (NormalFile) repoFile; 1056 if (!normalFile.isInProgress()) 1057 throw new IllegalStateException(String.format("NormalFile.inProgress == false! beginPutFile(...) not called?! repoFile=%s file=%s", 1058 repoFile, file)); 1059 1060 final FileWriteStrategy fileWriteStrategy = getFileWriteStrategy(file); 1061 logger.debug("endPutFile: fileWriteStrategy={}", fileWriteStrategy); 1062 1063 final File destFile = (fileWriteStrategy == FileWriteStrategy.replaceAfterTransfer 1064 ? createFile(file.getParentFile(), LocalRepoManager.TEMP_NEW_FILE_PREFIX + file.getName()) : file); 1065 1066 final InputStream fileIn; 1067 if (destFile != file) { 1068 try { 1069 fileIn = castStream(file.createInputStream()); 1070 destFile.createNewFile(); 1071 } catch (final IOException e) { 1072 throw new RuntimeException(e); 1073 } 1074 } 1075 else 1076 fileIn = null; 1077 1078 // tempChunkFileWithDtoFiles are sorted by offset (ascending) 1079 final Collection<TempChunkFileWithDtoFile> tempChunkFileWithDtoFiles = tempChunkFileManager.getOffset2TempChunkFileWithDtoFile(file).values(); 1080 try { 1081 final TempChunkFileDtoIo tempChunkFileDtoIo = new TempChunkFileDtoIo(); 1082 long destFileWriteOffset = 0; 1083 logger.debug("endPutFile: #tempChunkFileWithDtoFiles={}", tempChunkFileWithDtoFiles.size()); 1084 for (final TempChunkFileWithDtoFile tempChunkFileWithDtoFile : tempChunkFileWithDtoFiles) { 1085 final File tempChunkFile = tempChunkFileWithDtoFile.getTempChunkFile(); // tempChunkFile may be null!!! 1086 final File tempChunkFileDtoFile = tempChunkFileWithDtoFile.getTempChunkFileDtoFile(); 1087 if (tempChunkFileDtoFile == null) 1088 throw new IllegalStateException("No meta-data (tempChunkFileDtoFile) for file: " + (tempChunkFile == null ? null : tempChunkFile.getAbsolutePath())); 1089 1090 final TempChunkFileDto tempChunkFileDto = tempChunkFileDtoIo.deserialize(tempChunkFileDtoFile); 1091 final long offset = AssertUtil.assertNotNull(tempChunkFileDto.getFileChunkDto(), "tempChunkFileDto.fileChunkDto").getOffset(); 1092 1093 if (fileIn != null) { 1094 // The following might fail, if *file* was truncated during the transfer. In this case, 1095 // throwing an exception now is probably the best choice as the next sync run will 1096 // continue cleanly. 1097 logger.info("endPutFile: writing from fileIn into destFile {}", destFile.getName()); 1098 writeFileDataToDestFile(destFile, destFileWriteOffset, fileIn, offset - destFileWriteOffset); 1099 final long tempChunkFileLength = tempChunkFileDto.getFileChunkDto().getLength(); 1100 skipOrFail(fileIn, tempChunkFileLength); // skipping beyond the EOF is supported by the FileInputStream according to Javadoc. 1101 destFileWriteOffset = offset + tempChunkFileLength; 1102 } 1103 1104 if (tempChunkFile != null && tempChunkFile.exists()) { 1105 logger.info("endPutFile: writing tempChunkFile {} into destFile {}", tempChunkFile.getName(), destFile.getName()); 1106 writeTempChunkFileToDestFile(destFile, tempChunkFile, tempChunkFileDto); 1107 deleteOrFail(tempChunkFile); 1108 } 1109 } 1110 1111 if (fileIn != null && destFileWriteOffset < length) 1112 writeFileDataToDestFile(destFile, destFileWriteOffset, fileIn, length - destFileWriteOffset); 1113 1114 } finally { 1115 if (fileIn != null) 1116 fileIn.close(); 1117 } 1118 1119 try { 1120 final RandomAccessFile raf = destFile.createRandomAccessFile("rw"); 1121 try { 1122 raf.setLength(length); 1123 } finally { 1124 raf.close(); 1125 } 1126 } catch (final IOException e) { 1127 throw new RuntimeException(e); 1128 } 1129 1130 if (destFile != file) { 1131 deleteOrFail(file); 1132 destFile.renameTo(file); 1133 if (!file.exists()) 1134 throw new IllegalStateException(String.format("Renaming the file from '%s' to '%s' failed: The destination file does not exist.", destFile.getAbsolutePath(), file.getAbsolutePath())); 1135 1136 if (destFile.exists()) 1137 throw new IllegalStateException(String.format("Renaming the file from '%s' to '%s' failed: The source file still exists.", destFile.getAbsolutePath(), file.getAbsolutePath())); 1138 } 1139 1140 tempChunkFileManager.deleteTempChunkFiles(tempChunkFileWithDtoFiles); 1141 tempChunkFileManager.deleteTempDirIfEmpty(file); 1142 1143 final LocalRepoSync localRepoSync = LocalRepoSync.create(transaction); 1144 file.setLastModified(lastModified.getTime()); 1145 localRepoSync.updateRepoFile(normalFile, file, new NullProgressMonitor()); 1146 normalFile.setLastSyncFromRepositoryId(clientRepositoryId); 1147 normalFile.setInProgress(false); 1148 1149 logger.trace("endPutFile: Committing: sha1='{}' file='{}'", normalFile.getSha1(), file); 1150 if (sha1 != null && !sha1.equals(normalFile.getSha1())) { 1151 logger.warn("endPutFile: File was modified during transport (either on source or destination side): expectedSha1='{}' foundSha1='{}' file='{}'", 1152 sha1, normalFile.getSha1(), file); 1153 } 1154 1155 } catch (IOException x) { 1156 throw new RuntimeException(x); 1157 } finally { 1158 ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile); 1159 } 1160 transaction.commit(); 1161 } 1162 } 1163 1164 /** 1165 * Skip the given {@code length} number of bytes. 1166 * <p> 1167 * Because {@link InputStream#skip(long)} and {@link FileInputStream#skip(long)} are both documented to skip 1168 * over less than the requested number of bytes "for a number of reasons", this method invokes the underlying 1169 * skip(...) method multiple times until either EOF is reached or the requested number of bytes was skipped. 1170 * In case of EOF, an 1171 * @param in the {@link InputStream} to be skipped. Must not be <code>null</code>. 1172 * @param length the number of bytes to be skipped. Must not be negative (i.e. <code>length >= 0</code>). 1173 */ 1174 private void skipOrFail(final InputStream in, final long length) { 1175 AssertUtil.assertNotNull(in, "in"); 1176 if (length < 0) 1177 throw new IllegalArgumentException("length < 0"); 1178 1179 long skipped = 0; 1180 int skippedNowWas0Counter = 0; 1181 while (skipped < length) { 1182 final long toSkip = length - skipped; 1183 try { 1184 final long skippedNow = in.skip(toSkip); 1185 if (skippedNow < 0) 1186 throw new IOException("in.skip(" + toSkip + ") returned " + skippedNow); 1187 1188 if (skippedNow == 0) { 1189 if (++skippedNowWas0Counter >= 5) { 1190 throw new IOException(String.format( 1191 "Could not skip %s consecutive times!", skippedNowWas0Counter)); 1192 } 1193 } 1194 else 1195 skippedNowWas0Counter = 0; 1196 1197 skipped += skippedNow; 1198 } catch (final IOException e) { 1199 throw new RuntimeException(e); 1200 } 1201 } 1202 } 1203 1204 private void writeFileDataToDestFile(final File destFile, final long offset, final InputStream in, final long length) { 1205 AssertUtil.assertNotNull(destFile, "destFile"); 1206 AssertUtil.assertNotNull(in, "in"); 1207 if (offset < 0) 1208 throw new IllegalArgumentException("offset < 0"); 1209 1210 if (length == 0) 1211 return; 1212 1213 if (length < 0) 1214 throw new IllegalArgumentException("length < 0"); 1215 1216 long lengthDone = 0; 1217 1218 try { 1219 final RandomAccessFile raf = destFile.createRandomAccessFile("rw"); 1220 try { 1221 raf.seek(offset); 1222 1223 final byte[] buf = new byte[200 * 1024]; 1224 1225 while (lengthDone < length) { 1226 final long len = Math.min(length - lengthDone, buf.length); 1227 final int bytesRead = in.read(buf, 0, (int)len); 1228 if (bytesRead > 0) { 1229 raf.write(buf, 0, bytesRead); 1230 lengthDone += bytesRead; 1231 } 1232 else if (bytesRead < 0) 1233 throw new IOException("Premature end of stream!"); 1234 } 1235 } finally { 1236 raf.close(); 1237 } 1238 } catch (final IOException e) { 1239 throw new RuntimeException(e); 1240 } 1241 } 1242 1243 @Override 1244 public void endSyncFromRepository() { 1245 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 1246 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 1247 final PersistenceManager pm = ((co.codewizards.cloudstore.local.LocalRepoTransactionImpl)transaction).getPersistenceManager(); 1248 final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class); 1249 final LastSyncToRemoteRepoDao lastSyncToRemoteRepoDao = transaction.getDao(LastSyncToRemoteRepoDao.class); 1250 final ModificationDao modificationDao = transaction.getDao(ModificationDao.class); 1251 final TransferDoneMarkerDao transferDoneMarkerDao = transaction.getDao(TransferDoneMarkerDao.class); 1252 1253 final RemoteRepository toRemoteRepository = remoteRepositoryDao.getRemoteRepositoryOrFail(clientRepositoryId); 1254 1255 final LastSyncToRemoteRepo lastSyncToRemoteRepo = lastSyncToRemoteRepoDao.getLastSyncToRemoteRepoOrFail(toRemoteRepository); 1256 if (lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress() < 0) 1257 throw new IllegalStateException(String.format("lastSyncToRemoteRepo.localRepositoryRevisionInProgress < 0 :: There is no sync in progress for the RemoteRepository with entityID=%s", clientRepositoryId)); 1258 1259 lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress()); 1260 lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(-1); 1261 1262 pm.flush(); // prevent problems caused by batching, deletion and foreign keys 1263 final Collection<Modification> modifications = modificationDao.getModificationsBeforeOrEqual( 1264 toRemoteRepository, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()); 1265 modificationDao.deletePersistentAll(modifications); 1266 pm.flush(); 1267 1268 transferDoneMarkerDao.deleteRepoFileTransferDones(getRepositoryId(), clientRepositoryId); 1269 1270 final FileInProgressMarkerDao fileInProgressMarkerDao = transaction.getDao(FileInProgressMarkerDao.class); 1271 fileInProgressMarkerDao.deleteFileInProgressMarkers(getRepositoryId(), clientRepositoryId); 1272 1273 logger.info("endSyncFromRepository: localRepositoryId={} remoteRepositoryId={} localRepositoryRevisionSynced={}", 1274 getRepositoryId(), toRemoteRepository.getRepositoryId(), 1275 lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced()); 1276 1277 transaction.commit(); 1278 } 1279 } 1280 1281 @Override 1282 public void endSyncToRepository(final long fromLocalRevision) { 1283 final UUID clientRepositoryId = getClientRepositoryIdOrFail(); 1284 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 1285 final RemoteRepositoryDao remoteRepositoryDao = transaction.getDao(RemoteRepositoryDao.class); 1286 final TransferDoneMarkerDao transferDoneMarkerDao = transaction.getDao(TransferDoneMarkerDao.class); 1287 1288 final RemoteRepository remoteRepository = remoteRepositoryDao.getRemoteRepositoryOrFail(clientRepositoryId); 1289 remoteRepository.setRevision(fromLocalRevision); 1290 1291 transferDoneMarkerDao.deleteRepoFileTransferDones(clientRepositoryId, getRepositoryId()); 1292 1293 final FileInProgressMarkerDao fileInProgressMarkerDao = transaction.getDao(FileInProgressMarkerDao.class); 1294 fileInProgressMarkerDao.deleteFileInProgressMarkers(clientRepositoryId, getRepositoryId()); 1295 1296 logger.info("endSyncToRepository: localRepositoryId={} remoteRepositoryId={} transaction.localRevision={} remoteFromLocalRevision={}", 1297 getRepositoryId(), clientRepositoryId, 1298 transaction.getLocalRevision(), fromLocalRevision); 1299 1300 transaction.commit(); 1301 } 1302 } 1303 1304 @Override 1305 public boolean isTransferDone(final UUID fromRepositoryId, final UUID toRepositoryId, final TransferDoneMarkerType transferDoneMarkerType, final long fromEntityId, final long fromLocalRevision) { 1306 boolean result = false; 1307 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction(); ) { 1308 final TransferDoneMarkerDao dao = transaction.getDao(TransferDoneMarkerDao.class); 1309 final TransferDoneMarker transferDoneMarker = dao.getTransferDoneMarker( 1310 fromRepositoryId, toRepositoryId, transferDoneMarkerType, fromEntityId); 1311 if (transferDoneMarker != null) 1312 result = fromLocalRevision == transferDoneMarker.getFromLocalRevision(); 1313 1314 transaction.commit(); 1315 } 1316 return result; 1317 } 1318 1319 @Override 1320 public void markTransferDone(final UUID fromRepositoryId, final UUID toRepositoryId, final TransferDoneMarkerType transferDoneMarkerType, final long fromEntityId, final long fromLocalRevision) { 1321 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 1322 final TransferDoneMarkerDao dao = transaction.getDao(TransferDoneMarkerDao.class); 1323 TransferDoneMarker transferDoneMarker = dao.getTransferDoneMarker( 1324 fromRepositoryId, toRepositoryId, transferDoneMarkerType, fromEntityId); 1325 if (transferDoneMarker == null) { 1326 transferDoneMarker = new TransferDoneMarker(); 1327 transferDoneMarker.setFromRepositoryId(fromRepositoryId); 1328 transferDoneMarker.setToRepositoryId(toRepositoryId); 1329 transferDoneMarker.setTransferDoneMarkerType(transferDoneMarkerType); 1330 transferDoneMarker.setFromEntityId(fromEntityId); 1331 } 1332 transferDoneMarker.setFromLocalRevision(fromLocalRevision); 1333 dao.makePersistent(transferDoneMarker); 1334 1335 transaction.commit(); 1336 } 1337 } 1338 1339 @Override 1340 public Set<String> getFileInProgressPaths(final UUID fromRepository, final UUID toRepository) { 1341 try (final LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction();) { 1342 final FileInProgressMarkerDao dao = transaction.getDao(FileInProgressMarkerDao.class); 1343 final Collection<FileInProgressMarker> fileInProgressMarkers = dao.getFileInProgressMarkers(fromRepository, toRepository); 1344 final Set<String> paths = new HashSet<String>(fileInProgressMarkers.size()); 1345 for (final FileInProgressMarker fileInProgressMarker : fileInProgressMarkers) 1346 paths.add(fileInProgressMarker.getPath()); 1347 1348 transaction.commit(); 1349 return paths; 1350 } 1351 } 1352 1353 @Override 1354 public void markFileInProgress(final UUID fromRepository, final UUID toRepository, final String path, final boolean inProgress) { 1355 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { 1356 final FileInProgressMarkerDao dao = transaction.getDao(FileInProgressMarkerDao.class); 1357 FileInProgressMarker fileInProgressMarker = dao.getFileInProgressMarker(fromRepository, toRepository, path); 1358 1359 if (fileInProgressMarker == null && inProgress) { 1360 fileInProgressMarker = new FileInProgressMarker(); 1361 fileInProgressMarker.setFromRepositoryId(fromRepository); 1362 fileInProgressMarker.setToRepositoryId(toRepository); 1363 fileInProgressMarker.setPath(path); 1364 dao.makePersistent(fileInProgressMarker); 1365 logger.info("Storing fileInProgressMarker: {} on repo={}", fileInProgressMarker, getRepositoryId()); 1366 } else if (fileInProgressMarker != null && !inProgress) { 1367 logger.info("Removing fileInProgressMarker: {} on repo={}", fileInProgressMarker, getRepositoryId()); 1368 dao.deletePersistent(fileInProgressMarker); 1369 } else 1370 logger.warn("Unexpected state: markFileInProgress==null='{}', inProgress='{}' on repo={}", fileInProgressMarker == null, inProgress, getRepositoryId()); 1371 1372 transaction.commit(); 1373 } 1374 } 1375 1376 @Override 1377 public void putParentConfigPropSetDto(ConfigPropSetDto parentConfigPropSetDto) { 1378 try ( final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); ) { // we open a write-transaction merely for the exclusive lock 1379 final RemoteRepository remoteRepository = transaction.getDao(RemoteRepositoryDao.class).getRemoteRepositoryOrFail(getClientRepositoryIdOrFail()); 1380 if (! remoteRepository.getLocalPathPrefix().isEmpty()) { 1381 logger.warn("putParentConfigPropSetDto: IGNORING unsupported situation! See: https://github.com/cloudstore/cloudstore/issues/58"); 1382 return; 1383 } 1384 1385 final File metaDir = getLocalRepoManager().getLocalRoot().createFile(LocalRepoManager.META_DIR_NAME); 1386 if (! metaDir.isDirectory()) 1387 throw new IOException("Directory does not exist: " + metaDir); 1388 1389 final File repoParentConfigFile = metaDir.createFile(Config.PROPERTIES_FILE_NAME_PARENT_PREFIX + getClientRepositoryIdOrFail() + Config.PROPERTIES_FILE_NAME_SUFFIX); 1390 1391 if (parentConfigPropSetDto.getConfigPropDtos().isEmpty()) { 1392 repoParentConfigFile.delete(); 1393 if (repoParentConfigFile.isFile()) 1394 throw new IOException("Deleting file failed: " + repoParentConfigFile); 1395 } 1396 else { 1397 Properties properties = parentConfigPropSetDto.toProperties(); 1398 PropertiesUtil.store(repoParentConfigFile, properties, null); 1399 } 1400 1401 mergeRepoParentConfigFiles(); 1402 1403 transaction.commit(); 1404 } catch (IOException e) { 1405 throw new RuntimeException(e); 1406 } 1407 } 1408 1409 private void mergeRepoParentConfigFiles() throws IOException { 1410 final File metaDir = getLocalRepoManager().getLocalRoot().createFile(LocalRepoManager.META_DIR_NAME); 1411 1412 final Properties properties = new Properties(); 1413 for (File configFile : getRepoParentConfigFiles()) { 1414 try (InputStream in = castStream(configFile.createInputStream())) { 1415 properties.load(in); 1416 } 1417 } 1418 1419 final File parentConfigFile = metaDir.createFile(Config.PROPERTIES_FILE_NAME_PARENT); 1420 if (properties.isEmpty()) { 1421 parentConfigFile.delete(); 1422 if (parentConfigFile.isFile()) 1423 throw new IOException("Deleting file failed: " + parentConfigFile); 1424 } 1425 else 1426 PropertiesUtil.store(parentConfigFile, properties, null); 1427 } 1428 1429 private List<File> getRepoParentConfigFiles() { 1430 final List<File> result = new ArrayList<>(); 1431 final File metaDir = getLocalRepoManager().getLocalRoot().createFile(LocalRepoManager.META_DIR_NAME); 1432 1433 final Pattern repoParentConfigPattern = Pattern.compile( 1434 Pattern.quote(Config.PROPERTIES_FILE_NAME_PARENT_PREFIX) + "[^.]*" + Pattern.quote(Config.PROPERTIES_FILE_NAME_SUFFIX)); 1435 1436 Matcher repoParentConfigMatcher = null; 1437 for (File file : metaDir.listFiles()) { 1438 if (repoParentConfigMatcher == null) 1439 repoParentConfigMatcher = repoParentConfigPattern.matcher(file.getName()); 1440 else 1441 repoParentConfigMatcher.reset(file.getName()); 1442 1443 if (repoParentConfigMatcher.matches() && file.isFile()) 1444 result.add(file); 1445 } 1446 1447 Collections.sort(result, new Comparator<File>() { 1448 @Override 1449 public int compare(File o1, File o2) { 1450 return o1.getName().compareTo(o2.getName()); 1451 } 1452 }); 1453 1454 return result; 1455 } 1456 1457 @Override 1458 public VersionInfoDto getVersionInfoDto() { 1459 return VersionInfoProvider.getInstance().getVersionInfoDto(); 1460 } 1461}