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 &gt;= 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}