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