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