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