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