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