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