001package co.codewizards.cloudstore.local.transport;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004
005import java.io.ByteArrayInputStream;
006import java.io.File;
007import java.io.FileInputStream;
008import java.io.FileOutputStream;
009import java.io.IOException;
010import java.io.InputStream;
011import java.io.RandomAccessFile;
012import java.net.MalformedURLException;
013import java.net.URISyntaxException;
014import java.net.URL;
015import java.nio.file.Files;
016import java.nio.file.Path;
017import java.nio.file.Paths;
018import java.nio.file.StandardCopyOption;
019import java.security.NoSuchAlgorithmException;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.TreeMap;
029import java.util.UUID;
030import java.util.WeakHashMap;
031
032import javax.jdo.FetchPlan;
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.dto.ChangeSetDTO;
040import co.codewizards.cloudstore.core.dto.CopyModificationDTO;
041import co.codewizards.cloudstore.core.dto.DeleteModificationDTO;
042import co.codewizards.cloudstore.core.dto.DirectoryDTO;
043import co.codewizards.cloudstore.core.dto.FileChunkDTO;
044import co.codewizards.cloudstore.core.dto.ModificationDTO;
045import co.codewizards.cloudstore.core.dto.NormalFileDTO;
046import co.codewizards.cloudstore.core.dto.RepoFileDTO;
047import co.codewizards.cloudstore.core.dto.RepositoryDTO;
048import co.codewizards.cloudstore.core.dto.SymlinkDTO;
049import co.codewizards.cloudstore.core.dto.TempChunkFileDTO;
050import co.codewizards.cloudstore.core.dto.jaxb.TempChunkFileDTOIO;
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.DeleteModificationCollisionException;
059import co.codewizards.cloudstore.core.repo.transport.FileWriteStrategy;
060import co.codewizards.cloudstore.core.util.HashUtil;
061import co.codewizards.cloudstore.core.util.IOUtil;
062import co.codewizards.cloudstore.local.FilenameFilterSkipMetaDir;
063import co.codewizards.cloudstore.local.LocalRepoSync;
064import co.codewizards.cloudstore.local.LocalRepoTransactionImpl;
065import co.codewizards.cloudstore.local.persistence.CopyModification;
066import co.codewizards.cloudstore.local.persistence.DeleteModification;
067import co.codewizards.cloudstore.local.persistence.DeleteModificationDAO;
068import co.codewizards.cloudstore.local.persistence.Directory;
069import co.codewizards.cloudstore.local.persistence.FileChunk;
070import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepo;
071import co.codewizards.cloudstore.local.persistence.LastSyncToRemoteRepoDAO;
072import co.codewizards.cloudstore.local.persistence.LocalRepository;
073import co.codewizards.cloudstore.local.persistence.LocalRepositoryDAO;
074import co.codewizards.cloudstore.local.persistence.Modification;
075import co.codewizards.cloudstore.local.persistence.ModificationDAO;
076import co.codewizards.cloudstore.local.persistence.NormalFile;
077import co.codewizards.cloudstore.local.persistence.RemoteRepository;
078import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDAO;
079import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequest;
080import co.codewizards.cloudstore.local.persistence.RemoteRepositoryRequestDAO;
081import co.codewizards.cloudstore.local.persistence.RepoFile;
082import co.codewizards.cloudstore.local.persistence.RepoFileDAO;
083import co.codewizards.cloudstore.local.persistence.Symlink;
084
085public class FileRepoTransport extends AbstractRepoTransport {
086        private static final Logger logger = LoggerFactory.getLogger(FileRepoTransport.class);
087
088        private static final long MAX_REMOTE_REPOSITORY_REQUESTS_QUANTITY = 100; // TODO make configurable!
089
090        private static final String TEMP_CHUNK_FILE_PREFIX = "chunk_";
091        private static final String TEMP_CHUNK_FILE_DTO_FILE_SUFFIX = ".xml";
092
093        private LocalRepoManager localRepoManager;
094
095        @Override
096        public void close() {
097                if (localRepoManager != null) {
098                        logger.debug("close: Closing localRepoManager.");
099                        localRepoManager.close();
100                } else
101                        logger.debug("close: There is no localRepoManager.");
102
103                super.close();
104        }
105
106        @Override
107        public UUID getRepositoryId() {
108                return getLocalRepoManager().getRepositoryId();
109        }
110
111        @Override
112        public byte[] getPublicKey() {
113                return getLocalRepoManager().getPublicKey();
114        }
115
116        @Override
117        public void requestRepoConnection(byte[] publicKey) {
118                assertNotNull("publicKey", publicKey);
119                UUID clientRepositoryId = getClientRepositoryIdOrFail();
120                LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
121                try {
122                        RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class);
123                        RemoteRepository remoteRepository = remoteRepositoryDAO.getRemoteRepository(clientRepositoryId);
124                        if (remoteRepository != null)
125                                throw new IllegalArgumentException("RemoteRepository already connected! repositoryId=" + clientRepositoryId);
126
127                        String localPathPrefix = getPathPrefix();
128                        RemoteRepositoryRequestDAO remoteRepositoryRequestDAO = transaction.getDAO(RemoteRepositoryRequestDAO.class);
129                        RemoteRepositoryRequest remoteRepositoryRequest = remoteRepositoryRequestDAO.getRemoteRepositoryRequest(clientRepositoryId);
130                        if (remoteRepositoryRequest != null) {
131                                logger.info("RemoteRepository already requested to be connected. repositoryId={}", clientRepositoryId);
132
133                                // For security reasons, we do not allow to modify the public key! If we did,
134                                // an attacker might replace the public key while the user is verifying the public key's
135                                // fingerprint. The user would see & confirm the old public key, but the new public key
136                                // would be written to the RemoteRepository. This requires really lucky timing, but
137                                // if the attacker surveils the user, this might be feasable.
138                                if (!Arrays.equals(remoteRepositoryRequest.getPublicKey(), publicKey))
139                                        throw new IllegalStateException("Cannot modify the public key! Use 'dropRepoConnection' to drop the old request or wait until it expired.");
140
141                                // For the same reasons stated above, we do not allow changing the local path-prefix, too.
142                                if (!remoteRepositoryRequest.getLocalPathPrefix().equals(localPathPrefix))
143                                        throw new IllegalStateException("Cannot modify the local path-prefix! Use 'dropRepoConnection' to drop the old request or wait until it expired.");
144
145                                remoteRepositoryRequest.setChanged(new Date()); // make sure it is not deleted soon (the request expires after a while)
146                        }
147                        else {
148                                long remoteRepositoryRequestsCount = remoteRepositoryRequestDAO.getObjectsCount();
149                                if (remoteRepositoryRequestsCount >= MAX_REMOTE_REPOSITORY_REQUESTS_QUANTITY)
150                                        throw new IllegalStateException(String.format(
151                                                        "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));
152
153                                remoteRepositoryRequest = new RemoteRepositoryRequest();
154                                remoteRepositoryRequest.setRepositoryId(clientRepositoryId);
155                                remoteRepositoryRequest.setPublicKey(publicKey);
156                                remoteRepositoryRequest.setLocalPathPrefix(localPathPrefix);
157                                remoteRepositoryRequestDAO.makePersistent(remoteRepositoryRequest);
158                        }
159
160                        transaction.commit();
161                } finally {
162                        transaction.rollbackIfActive();
163                }
164        }
165
166        @Override
167        public RepositoryDTO getRepositoryDTO() {
168                LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction();
169                try {
170                        LocalRepositoryDAO localRepositoryDAO = transaction.getDAO(LocalRepositoryDAO.class);
171                        LocalRepository localRepository = localRepositoryDAO.getLocalRepositoryOrFail();
172                        RepositoryDTO repositoryDTO = toRepositoryDTO(localRepository);
173                        transaction.commit();
174                        return repositoryDTO;
175                } finally {
176                        transaction.rollbackIfActive();
177                }
178        }
179
180        @Override
181        public ChangeSetDTO getChangeSetDTO(final boolean localSync) {
182                if (localSync)
183                        getLocalRepoManager().localSync(new LoggerProgressMonitor(logger));
184
185                final UUID clientRepositoryId = getClientRepositoryIdOrFail();
186                final ChangeSetDTO changeSetDTO = new ChangeSetDTO();
187                final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); // It writes the LastSyncToRemoteRepo!
188                try {
189                        LocalRepositoryDAO localRepositoryDAO = transaction.getDAO(LocalRepositoryDAO.class);
190                        RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class);
191                        LastSyncToRemoteRepoDAO lastSyncToRemoteRepoDAO = transaction.getDAO(LastSyncToRemoteRepoDAO.class);
192                        ModificationDAO modificationDAO = transaction.getDAO(ModificationDAO.class);
193                        RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class);
194
195                        // We must *first* read the LocalRepository and afterwards all changes, because this way, we don't need to lock it in the DB.
196                        // If we *then* read RepoFiles with a newer localRevision, it doesn't do any harm - we'll simply read them again, in the
197                        // next run.
198                        LocalRepository localRepository = localRepositoryDAO.getLocalRepositoryOrFail();
199                        changeSetDTO.setRepositoryDTO(toRepositoryDTO(localRepository));
200
201                        RemoteRepository toRemoteRepository = remoteRepositoryDAO.getRemoteRepositoryOrFail(clientRepositoryId);
202
203                        LastSyncToRemoteRepo lastSyncToRemoteRepo = lastSyncToRemoteRepoDAO.getLastSyncToRemoteRepo(toRemoteRepository);
204                        if (lastSyncToRemoteRepo == null) {
205                                lastSyncToRemoteRepo = new LastSyncToRemoteRepo();
206                                lastSyncToRemoteRepo.setRemoteRepository(toRemoteRepository);
207                                lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(-1);
208                        }
209                        lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(localRepository.getRevision());
210                        lastSyncToRemoteRepoDAO.makePersistent(lastSyncToRemoteRepo);
211
212                        ((LocalRepoTransactionImpl)transaction).getPersistenceManager().getFetchPlan().setGroup(FetchPlan.ALL);
213                        Collection<Modification> modifications = modificationDAO.getModificationsAfter(toRemoteRepository, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced());
214                        changeSetDTO.setModificationDTOs(toModificationDTOs(modifications));
215
216                        if (!getPathPrefix().isEmpty()) {
217                                Collection<DeleteModification> deleteModifications = transaction.getDAO(DeleteModificationDAO.class).getDeleteModificationsForPathOrParentOfPathAfter(
218                                                getPathPrefix(), lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), toRemoteRepository);
219                                if (!deleteModifications.isEmpty()) { // our virtual root was deleted => create synthetic DeleteModificationDTO for virtual root
220                                        DeleteModificationDTO deleteModificationDTO = new DeleteModificationDTO();
221                                        deleteModificationDTO.setId(0);
222                                        deleteModificationDTO.setLocalRevision(localRepository.getRevision());
223                                        deleteModificationDTO.setPath("");
224                                        changeSetDTO.getModificationDTOs().add(deleteModificationDTO);
225                                }
226                        }
227
228                        final Collection<RepoFile> repoFiles = repoFileDAO.getRepoFilesChangedAfterExclLastSyncFromRepositoryId(
229                                        lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced(), clientRepositoryId);
230                        RepoFile pathPrefixRepoFile = null; // the virtual root for the client
231                        if (!getPathPrefix().isEmpty()) {
232                                pathPrefixRepoFile = repoFileDAO.getRepoFile(getLocalRepoManager().getLocalRoot(), getPathPrefixFile());
233                        }
234                        Map<Long, RepoFileDTO> id2RepoFileDTO = getId2RepoFileDTOWithParents(pathPrefixRepoFile, repoFiles, repoFileDAO);
235                        changeSetDTO.setRepoFileDTOs(new ArrayList<RepoFileDTO>(id2RepoFileDTO.values()));
236
237                        transaction.commit();
238                        return changeSetDTO;
239                } finally {
240                        transaction.rollbackIfActive();
241                }
242        }
243
244        protected File getPathPrefixFile() {
245                String pathPrefix = getPathPrefix();
246                if (pathPrefix.isEmpty())
247                        return getLocalRepoManager().getLocalRoot();
248                else
249                        return new File(getLocalRepoManager().getLocalRoot(), pathPrefix);
250        }
251
252        @Override
253        public void makeDirectory(String path, Date lastModified) {
254                path = prefixPath(path);
255                File file = getFile(path);
256                UUID clientRepositoryId = getClientRepositoryIdOrFail();
257                LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
258                try {
259                        assertNoDeleteModificationCollision(transaction, clientRepositoryId, path);
260                        mkDir(transaction, clientRepositoryId, file, lastModified);
261                        transaction.commit();
262                } finally {
263                        transaction.rollbackIfActive();
264                }
265        }
266
267        @Override
268        public void makeSymlink(String path, String target, Date lastModified) {
269                path = prefixPath(path);
270                assertNotNull("target", target);
271                final File file = getFile(path);
272                final UUID clientRepositoryId = getClientRepositoryIdOrFail();
273                final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
274                try {
275                        final RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class);
276
277                        final File parentFile = file.getParentFile();
278                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile);
279                        try {
280                                assertNoDeleteModificationCollision(transaction, clientRepositoryId, path);
281
282                                if (file.exists() && !isSymlink(file))
283                                        file.renameTo(IOUtil.createCollisionFile(file));
284
285                                if (file.exists() && !isSymlink(file))
286                                        throw new IllegalStateException("Could not rename file! It is still in the way: " + file);
287
288                                final File localRoot = getLocalRepoManager().getLocalRoot();
289
290                                try {
291                                        final boolean currentTargetEqualsNewTarget;
292                                        final Path symlinkPath = file.toPath();
293                                        if (Files.isSymbolicLink(file.toPath()) || file.exists()) {
294                                                final Path currentTargetPath = Files.readSymbolicLink(symlinkPath);
295                                                final String currentTarget = IOUtil.toPathString(currentTargetPath);
296                                                currentTargetEqualsNewTarget = currentTarget.equals(target);
297                                                if (!currentTargetEqualsNewTarget) {
298                                                        final RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file);
299                                                        if (repoFile == null) {
300                                                                File collisionFile = IOUtil.createCollisionFile(file);
301                                                                file.renameTo(collisionFile);
302                                                                if (file.exists())
303                                                                        throw new IllegalStateException("Could not rename file to resolve collision: " + file);
304                                                        }
305                                                        else
306                                                                detectAndHandleFileCollision(transaction, clientRepositoryId, parentFile, repoFile);
307
308                                                        file.delete();
309                                                }
310                                        }
311                                        else
312                                                currentTargetEqualsNewTarget = false;
313
314                                        if (!currentTargetEqualsNewTarget)
315                                                Files.createSymbolicLink(symlinkPath, Paths.get(target));
316
317                                        if (lastModified != null)
318                                                IOUtil.setLastModifiedNoFollow(file, lastModified.getTime());
319
320                                } catch (IOException e) {
321                                        throw new RuntimeException(e);
322                                }
323
324                                new LocalRepoSync(transaction).sync(file, new NullProgressMonitor());
325
326                                Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles = getOffset2TempChunkFileWithDTOFile(file).values();
327                                for (TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) {
328                                        if (tempChunkFileWithDTOFile.getTempChunkFileDTOFile() != null)
329                                                deleteOrFail(tempChunkFileWithDTOFile.getTempChunkFileDTOFile());
330
331                                        if (tempChunkFileWithDTOFile.getTempChunkFile() != null)
332                                                deleteOrFail(tempChunkFileWithDTOFile.getTempChunkFile());
333                                }
334
335                                final RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file);
336                                if (repoFile == null)
337                                        throw new IllegalStateException("LocalRepoSync.sync(...) did not create the RepoFile for file: " + file);
338
339                                if (!(repoFile instanceof Symlink))
340                                        throw new IllegalStateException("LocalRepoSync.sync(...) created an instance of " + repoFile.getClass().getName() + " instead  of a Symlink for file: " + file);
341
342                                repoFile.setLastSyncFromRepositoryId(clientRepositoryId);
343
344                        } finally {
345                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile);
346                        }
347
348                        transaction.commit();
349                } finally {
350                        transaction.rollbackIfActive();
351                }
352        }
353
354        private void assertNoDeleteModificationCollision(LocalRepoTransaction transaction, UUID fromRepositoryId, String path) throws DeleteModificationCollisionException {
355                RemoteRepository fromRemoteRepository = transaction.getDAO(RemoteRepositoryDAO.class).getRemoteRepositoryOrFail(fromRepositoryId);
356                long lastSyncFromRemoteRepositoryLocalRevision = fromRemoteRepository.getLocalRevision();
357
358                if (!path.startsWith("/"))
359                        path = '/' + path;
360
361                DeleteModificationDAO deleteModificationDAO = transaction.getDAO(DeleteModificationDAO.class);
362                Collection<DeleteModification> deleteModifications = deleteModificationDAO.getDeleteModificationsForPathOrParentOfPathAfter(
363                                path, lastSyncFromRemoteRepositoryLocalRevision, fromRemoteRepository);
364
365                if (!deleteModifications.isEmpty())
366                        throw new DeleteModificationCollisionException(
367                                        String.format("There is at least one DeleteModification for repositoryId=%s path='%s'", fromRepositoryId, path));
368        }
369
370        @Override
371        public void copy(String fromPath, String toPath) {
372                fromPath = prefixPath(fromPath);
373                toPath = prefixPath(toPath);
374
375                final File fromFile = getFile(fromPath);
376                final File toFile = getFile(toPath);
377
378                if (!fromFile.exists()) // TODO throw an exception and catch in RepoToRepoSync!
379                        return;
380
381                if (toFile.exists()) // TODO either simply throw an exception or implement proper collision check.
382                        return;
383
384                final File toParentFile = toFile.getParentFile();
385                final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
386                try {
387                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(toParentFile);
388                        try {
389                                try {
390                                        if (!toParentFile.isDirectory())
391                                                toParentFile.mkdirs();
392
393                                        Files.copy(fromFile.toPath(), toFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES);
394                                } catch (IOException e) {
395                                        throw new RuntimeException(e);
396                                }
397
398                                final LocalRepoSync localRepoSync = new LocalRepoSync(transaction);
399                                final RepoFile toRepoFile = localRepoSync.sync(toFile, new NullProgressMonitor());
400                                assertNotNull("toRepoFile", toRepoFile);
401                                toRepoFile.setLastSyncFromRepositoryId(getClientRepositoryIdOrFail());
402                        } finally {
403                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(toParentFile);
404                        }
405                        transaction.commit();
406                } finally {
407                        transaction.rollbackIfActive();
408                }
409        }
410
411        @Override
412        public void move(String fromPath, String toPath) {
413                fromPath = prefixPath(fromPath);
414                toPath = prefixPath(toPath);
415
416                final File fromFile = getFile(fromPath);
417                final File toFile = getFile(toPath);
418
419                if (!fromFile.exists()) // TODO throw an exception and catch in RepoToRepoSync!
420                        return;
421
422                if (toFile.exists()) // TODO either simply throw an exception or implement proper collision check.
423                        return;
424
425                final File fromParentFile = fromFile.getParentFile();
426                final File toParentFile = toFile.getParentFile();
427                LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
428                try {
429                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(fromParentFile);
430                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(toParentFile);
431                        try {
432                                try {
433                                        if (!toParentFile.isDirectory())
434                                                toParentFile.mkdirs();
435
436                                        Files.move(fromFile.toPath(), toFile.toPath());
437                                } catch (IOException e) {
438                                        throw new RuntimeException(e);
439                                }
440
441                                final LocalRepoSync localRepoSync = new LocalRepoSync(transaction);
442                                final RepoFile toRepoFile = localRepoSync.sync(toFile, new NullProgressMonitor());
443                                final RepoFile fromRepoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fromFile);
444                                if (fromRepoFile != null)
445                                        localRepoSync.deleteRepoFile(fromRepoFile);
446
447                                assertNotNull("toRepoFile", toRepoFile);
448                                toRepoFile.setLastSyncFromRepositoryId(getClientRepositoryIdOrFail());
449                        } finally {
450                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(fromParentFile);
451                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(toParentFile);
452                        }
453                        transaction.commit();
454                } finally {
455                        transaction.rollbackIfActive();
456                }
457        }
458
459        @Override
460        public void delete(String path) {
461                path = prefixPath(path);
462                File file = getFile(path);
463                UUID clientRepositoryId = getClientRepositoryIdOrFail();
464                boolean fileIsLocalRoot = localRepoManager.getLocalRoot().equals(file);
465                File parentFile = file.getParentFile();
466                LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
467                try {
468                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile);
469                        try {
470                                LocalRepoSync localRepoSync = new LocalRepoSync(transaction);
471                                localRepoSync.sync(file, new NullProgressMonitor());
472
473                                if (fileIsLocalRoot) {
474                                        // Cannot delete the repository's root! Deleting all its contents instead.
475                                        long fileLastModified = file.lastModified();
476                                        try {
477                                                File[] children = file.listFiles(new FilenameFilterSkipMetaDir());
478                                                if (children == null)
479                                                        throw new IllegalStateException("File-listing localRoot returned null: " + file);
480
481                                                for (File child : children)
482                                                        delete(transaction, localRepoSync, clientRepositoryId, child);
483                                        } finally {
484                                                file.setLastModified(fileLastModified);
485                                        }
486                                }
487                                else
488                                        delete(transaction, localRepoSync, clientRepositoryId, file);
489
490                        } finally {
491                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile);
492                        }
493                        transaction.commit();
494                } finally {
495                        transaction.rollbackIfActive();
496                }
497        }
498
499        private void delete(LocalRepoTransaction transaction, LocalRepoSync localRepoSync, UUID fromRepositoryId, File file) {
500                if (detectFileCollisionRecursively(transaction, fromRepositoryId, file)) {
501                        file.renameTo(IOUtil.createCollisionFile(file));
502
503                        if (file.exists())
504                                throw new IllegalStateException("Renaming file failed: " + file);
505                }
506
507                if (!IOUtil.deleteDirectoryRecursively(file)) {
508                        throw new IllegalStateException("Deleting file or directory failed: " + file);
509                }
510
511                RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), file);
512                if (repoFile != null)
513                        localRepoSync.deleteRepoFile(repoFile);
514        }
515
516        @Override
517        public RepoFileDTO getRepoFileDTO(String path) {
518                RepoFileDTO repoFileDTO = null;
519                path = prefixPath(path);
520                File file = getFile(path);
521                LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction(); // it performs a local sync!
522                try {
523                        LocalRepoSync localRepoSync = new LocalRepoSync(transaction);
524                        localRepoSync.sync(file, new NullProgressMonitor());
525
526                        RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class);
527                        RepoFile repoFile = repoFileDAO.getRepoFile(getLocalRepoManager().getLocalRoot(), file);
528                        if (repoFile != null)
529                                repoFileDTO = toRepoFileDTO(repoFile, repoFileDAO, Integer.MAX_VALUE); // TODO pass depth as argument - or maybe leave it this way?
530
531                        transaction.commit();
532                } catch (RuntimeException x) {
533                        throw x;
534                } catch (Exception x) {
535                        throw new RuntimeException(x);
536                } finally {
537                        transaction.rollbackIfActive();
538                }
539                return repoFileDTO;
540        }
541
542        private FileChunkDTO toFileChunkDTO(FileChunk fileChunk) {
543                final FileChunkDTO fileChunkDTO = new FileChunkDTO();
544                fileChunkDTO.setOffset(fileChunk.getOffset());
545                fileChunkDTO.setLength(fileChunk.getLength());
546                fileChunkDTO.setSha1(fileChunk.getSha1());
547                return fileChunkDTO;
548        }
549
550        /**
551         * @param offset the offset in the (real) destination file (<i>not</i> in {@code tempChunkFile}! there the offset is always 0).
552         * @param tempChunkFile the tempChunkFile containing the chunk's data. Must not be <code>null</code>.
553         * @param sha1 the sha1 of the single chunk (in {@code tempChunkFile}). Must not be <code>null</code>.
554         * @return the DTO. Never <code>null</code>.
555         */
556        private TempChunkFileDTO createTempChunkFileDTO(final long offset, final File tempChunkFile, final String sha1) {
557                assertNotNull("tempChunkFile", tempChunkFile);
558                assertNotNull("sha1", sha1);
559
560                if (!tempChunkFile.exists())
561                        throw new IllegalArgumentException("The tempChunkFile does not exist: " + tempChunkFile.getAbsolutePath());
562
563                final FileChunkDTO fileChunkDTO = new FileChunkDTO();
564                fileChunkDTO.setOffset(offset);
565
566                final long tempChunkFileLength = tempChunkFile.length();
567                if (tempChunkFileLength > Integer.MAX_VALUE)
568                        throw new IllegalStateException("tempChunkFile.length > Integer.MAX_VALUE");
569
570                fileChunkDTO.setLength((int) tempChunkFileLength);
571                fileChunkDTO.setSha1(sha1);
572
573                final TempChunkFileDTO tempChunkFileDTO = new TempChunkFileDTO();
574                tempChunkFileDTO.setFileChunkDTO(fileChunkDTO);
575                return tempChunkFileDTO;
576        }
577
578        protected LocalRepoManager getLocalRepoManager() {
579                if (localRepoManager == null) {
580                        logger.debug("getLocalRepoManager: Creating a new LocalRepoManager.");
581                        File remoteRootFile;
582                        try {
583                                remoteRootFile = new File(getRemoteRootWithoutPathPrefix().toURI());
584                        } catch (URISyntaxException e) {
585                                throw new RuntimeException(e);
586                        }
587                        localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(remoteRootFile);
588                }
589                return localRepoManager;
590        }
591
592        @Override
593        protected URL determineRemoteRootWithoutPathPrefix() {
594                File remoteRootFile;
595                try {
596                        remoteRootFile = new File(getRemoteRoot().toURI());
597                } catch (URISyntaxException e) {
598                        throw new RuntimeException(e);
599                }
600
601                File localRootFile = LocalRepoHelper.getLocalRootContainingFile(remoteRootFile);
602                if (localRootFile == null)
603                        throw new IllegalStateException(String.format(
604                                        "remoteRoot='%s' does not point to a file or directory within an existing repository (nor its root directory)!",
605                                        getRemoteRoot()));
606
607                try {
608                        return localRootFile.toURI().toURL();
609                } catch (MalformedURLException e) {
610                        throw new RuntimeException(e);
611                }
612        }
613
614        private List<ModificationDTO> toModificationDTOs(Collection<Modification> modifications) {
615                long startTimestamp = System.currentTimeMillis();
616                List<ModificationDTO> result = new ArrayList<ModificationDTO>(assertNotNull("modifications", modifications).size());
617                for (Modification modification : modifications) {
618                        ModificationDTO modificationDTO = toModificationDTO(modification);
619                        if (modificationDTO != null)
620                                result.add(modificationDTO);
621                }
622                logger.debug("toModificationDTOs: Creating {} ModificationDTOs took {} ms.", result.size(), System.currentTimeMillis() - startTimestamp);
623                return result;
624        }
625
626        private ModificationDTO toModificationDTO(Modification modification) {
627                ModificationDTO modificationDTO;
628                if (modification instanceof CopyModification) {
629                        CopyModification copyModification = (CopyModification) modification;
630
631                        String fromPath = copyModification.getFromPath();
632                        String toPath = copyModification.getToPath();
633                        if (!isPathUnderPathPrefix(fromPath) || !isPathUnderPathPrefix(toPath))
634                                return null;
635
636                        fromPath = unprefixPath(fromPath);
637                        toPath = unprefixPath(toPath);
638
639                        CopyModificationDTO copyModificationDTO = new CopyModificationDTO();
640                        modificationDTO = copyModificationDTO;
641                        copyModificationDTO.setFromPath(fromPath);
642                        copyModificationDTO.setToPath(toPath);
643                }
644                else if (modification instanceof DeleteModification) {
645                        DeleteModification deleteModification = (DeleteModification) modification;
646
647                        String path = deleteModification.getPath();
648                        if (!isPathUnderPathPrefix(path))
649                                return null;
650
651                        path = unprefixPath(path);
652
653                        DeleteModificationDTO deleteModificationDTO;
654                        modificationDTO = deleteModificationDTO = new DeleteModificationDTO();
655                        deleteModificationDTO.setPath(path);
656                }
657                else
658                        throw new IllegalArgumentException("Unknown modification type: " + modification);
659
660                modificationDTO.setId(modification.getId());
661                modificationDTO.setLocalRevision(modification.getLocalRevision());
662
663                return modificationDTO;
664        }
665
666        private RepositoryDTO toRepositoryDTO(LocalRepository localRepository) {
667                RepositoryDTO repositoryDTO = new RepositoryDTO();
668                repositoryDTO.setRepositoryId(localRepository.getRepositoryId());
669                repositoryDTO.setRevision(localRepository.getRevision());
670                repositoryDTO.setPublicKey(localRepository.getPublicKey());
671                return repositoryDTO;
672        }
673
674        private Map<Long, RepoFileDTO> getId2RepoFileDTOWithParents(RepoFile pathPrefixRepoFile, Collection<RepoFile> repoFiles, RepoFileDAO repoFileDAO) {
675                assertNotNull("repoFileDAO", repoFileDAO);
676                assertNotNull("repoFiles", repoFiles);
677                Map<Long, RepoFileDTO> entityID2RepoFileDTO = new HashMap<Long, RepoFileDTO>();
678                for (RepoFile repoFile : repoFiles) {
679                        RepoFile rf = repoFile;
680                        if (rf instanceof NormalFile) {
681                                NormalFile nf = (NormalFile) rf;
682                                if (nf.isInProgress()) {
683                                        continue;
684                                }
685                        }
686
687                        if (pathPrefixRepoFile != null && !isDirectOrIndirectParent(pathPrefixRepoFile, rf))
688                                continue;
689
690                        while (rf != null) {
691                                if (!entityID2RepoFileDTO.containsKey(rf.getId())) {
692                                        RepoFileDTO repoFileDTO = toRepoFileDTO(rf, repoFileDAO, 0);
693                                        if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf)) {
694                                                repoFileDTO.setParentId(null); // virtual root has no parent!
695                                                repoFileDTO.setName(""); // virtual root has no name!
696                                        }
697
698                                        entityID2RepoFileDTO.put(rf.getId(), repoFileDTO);
699                                }
700
701                                if (pathPrefixRepoFile != null && pathPrefixRepoFile.equals(rf))
702                                        break;
703
704                                rf = rf.getParent();
705                        }
706                }
707                return entityID2RepoFileDTO;
708        }
709
710        private boolean isDirectOrIndirectParent(RepoFile parentRepoFile, RepoFile repoFile) {
711                assertNotNull("parentRepoFile", parentRepoFile);
712                assertNotNull("repoFile", repoFile);
713                RepoFile rf = repoFile;
714                while (rf != null) {
715                        if (parentRepoFile.equals(rf))
716                                return true;
717
718                        rf = rf.getParent();
719                }
720                return false;
721        }
722
723        private RepoFileDTO toRepoFileDTO(RepoFile repoFile, RepoFileDAO repoFileDAO, int depth) {
724                assertNotNull("repoFileDAO", repoFileDAO);
725                assertNotNull("repoFile", repoFile);
726                final RepoFileDTO repoFileDTO;
727                if (repoFile instanceof NormalFile) {
728                        final NormalFile normalFile = (NormalFile) repoFile;
729                        final NormalFileDTO normalFileDTO;
730                        repoFileDTO = normalFileDTO = new NormalFileDTO();
731                        normalFileDTO.setLength(normalFile.getLength());
732                        normalFileDTO.setSha1(normalFile.getSha1());
733                        if (depth > 0) {
734                                // TODO this should actually be a SortedSet, but for whatever reason, I started
735                                // getting ClassCastExceptions and had to switch to a normal Set :-(
736                                final List<FileChunk> fileChunks = new ArrayList<>(normalFile.getFileChunks());
737                                Collections.sort(fileChunks);
738                                for (final FileChunk fileChunk : fileChunks) {
739                                        normalFileDTO.getFileChunkDTOs().add(toFileChunkDTO(fileChunk));
740                                }
741                        }
742                        if (depth > 1) {
743                                final TempChunkFileDTOIO tempChunkFileDTOIO = new TempChunkFileDTOIO();
744                                final File file = repoFile.getFile(getLocalRepoManager().getLocalRoot());
745                                for (TempChunkFileWithDTOFile tempChunkFileWithDTOFile : getOffset2TempChunkFileWithDTOFile(file).values()) {
746                                        final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile();
747                                        if (tempChunkFileDTOFile == null)
748                                                continue; // incomplete: meta-data not yet written => ignore
749
750                                        final TempChunkFileDTO tempChunkFileDTO;
751                                        try {
752                                                tempChunkFileDTO = tempChunkFileDTOIO.deserialize(tempChunkFileDTOFile);
753                                        } catch (Exception x) {
754                                                logger.warn("toRepoFileDTO: Ignoring corrupt tempChunkFileDTOFile '" + tempChunkFileDTOFile.getAbsolutePath() + "': " + x, x);
755                                                continue;
756                                        }
757                                        normalFileDTO.getTempFileChunkDTOs().add(assertNotNull("tempChunkFileDTO.fileChunkDTO", tempChunkFileDTO.getFileChunkDTO()));
758                                }
759                        }
760                }
761                else if (repoFile instanceof Directory) {
762                        repoFileDTO = new DirectoryDTO();
763                }
764                else if (repoFile instanceof Symlink) {
765                        final Symlink symlink = (Symlink) repoFile;
766                        final SymlinkDTO symlinkDTO;
767                        repoFileDTO = symlinkDTO = new SymlinkDTO();
768                        symlinkDTO.setTarget(symlink.getTarget());
769                }
770                else
771                        throw new UnsupportedOperationException("RepoFile type not yet supported: " + repoFile);
772
773                repoFileDTO.setId(repoFile.getId());
774                repoFileDTO.setLocalRevision(repoFile.getLocalRevision());
775                repoFileDTO.setName(repoFile.getName());
776                repoFileDTO.setParentId(repoFile.getParent() == null ? null : repoFile.getParent().getId());
777                repoFileDTO.setLastModified(repoFile.getLastModified());
778
779                return repoFileDTO;
780        }
781
782        private void mkDir(LocalRepoTransaction transaction, UUID clientRepositoryId, File file, Date lastModified) {
783                assertNotNull("transaction", transaction);
784                assertNotNull("file", file);
785
786                final File localRoot = getLocalRepoManager().getLocalRoot();
787                if (localRoot.equals(file)) {
788                        return;
789                }
790
791                final File parentFile = file.getParentFile();
792                ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile);
793                try {
794                        RepoFile parentRepoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, parentFile);
795
796                        if (!localRoot.equals(parentFile) && (!parentFile.isDirectory() || parentRepoFile == null))
797                                mkDir(transaction, clientRepositoryId, parentFile, null);
798
799                        if (parentRepoFile == null)
800                                parentRepoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, parentFile);
801
802                        if (parentRepoFile == null) // now, it should definitely not be null anymore!
803                                throw new IllegalStateException("parentRepoFile == null");
804
805                        if (file.exists() && !file.isDirectory())
806                                file.renameTo(IOUtil.createCollisionFile(file));
807
808                        if (file.exists() && !file.isDirectory())
809                                throw new IllegalStateException("Could not rename file! It is still in the way: " + file);
810
811                        if (!file.isDirectory())
812                                file.mkdir();
813
814                        if (!file.isDirectory())
815                                throw new IllegalStateException("Could not create directory (permissions?!): " + file);
816
817                        RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, file);
818                        if (repoFile != null && !(repoFile instanceof Directory)) {
819                                transaction.getDAO(RepoFileDAO.class).deletePersistent(repoFile);
820                                repoFile = null;
821                        }
822
823                        if (lastModified != null)
824                                file.setLastModified(lastModified.getTime());
825
826                        if (repoFile == null) {
827                                Directory directory;
828                                repoFile = directory = new Directory();
829                                directory.setName(file.getName());
830                                directory.setParent(parentRepoFile);
831                                directory.setLastModified(new Date(file.lastModified()));
832                                repoFile = directory = transaction.getDAO(RepoFileDAO.class).makePersistent(directory);
833                        }
834                        else if (repoFile.getLastModified().getTime() != file.lastModified())
835                                repoFile.setLastModified(new Date(file.lastModified()));
836
837                        repoFile.setLastSyncFromRepositoryId(clientRepositoryId);
838                } finally {
839                        ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile);
840                }
841        }
842
843        /**
844         * @param path the prefixed path (relative to the real root).
845         * @return the file in the local repository. Never <code>null</code>.
846         */
847        protected File getFile(String path) {
848                path = assertNotNull("path", path).replace('/', File.separatorChar);
849                File file = new File(getLocalRepoManager().getLocalRoot(), path);
850                return file;
851        }
852
853        @Override
854        public byte[] getFileData(String path, long offset, int length) {
855                path = prefixPath(path);
856                File file = getFile(path);
857                try {
858                        RandomAccessFile raf = new RandomAccessFile(file, "r");
859                        try {
860                                raf.seek(offset);
861                                if (length < 0) {
862                                        long l = raf.length() - offset;
863                                        if (l > Integer.MAX_VALUE)
864                                                throw new IllegalArgumentException(
865                                                                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.",
866                                                                                path, offset, length, Integer.MAX_VALUE));
867
868                                        length = (int) l;
869                                }
870
871                                byte[] bytes = new byte[length];
872                                int off = 0;
873                                int numRead = 0;
874                                while (off < bytes.length && (numRead = raf.read(bytes, off, bytes.length-off)) >= 0) {
875                                        off += numRead;
876                                }
877
878                                if (off < bytes.length) // Read INCOMPLETELY => discarding
879                                        return null;
880
881                                return bytes;
882                        } finally {
883                                raf.close();
884                        }
885                } catch (IOException e) {
886                        throw new RuntimeException(e);
887                }
888        }
889
890        @Override
891        public void beginPutFile(String path) {
892                path = prefixPath(path);
893                final File file = getFile(path); // null-check already inside getFile(...) - no need for another check here
894                final UUID clientRepositoryId = getClientRepositoryIdOrFail();
895                final File parentFile = file.getParentFile();
896                final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
897                try {
898                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile);
899                        try {
900                                if (isSymlink(file) || (file.exists() && !file.isFile())) { // exists() and isFile() both resolve symlinks! Their result depends on where the symlink points to.
901                                        logger.info("beginPutFile: Collision: Destination file already exists and is a symlink or a directory! file='{}'", file.getAbsolutePath());
902                                        final File collisionFile = IOUtil.createCollisionFile(file);
903                                        file.renameTo(collisionFile);
904                                        new LocalRepoSync(transaction).sync(collisionFile, new NullProgressMonitor());
905                                }
906
907                                if (isSymlink(file) || (file.exists() && !file.isFile()))
908                                        throw new IllegalStateException("Could not rename file! It is still in the way: " + file);
909
910                                final File localRoot = getLocalRepoManager().getLocalRoot();
911                                assertNoDeleteModificationCollision(transaction, clientRepositoryId, path);
912
913                                boolean newFile = false;
914                                if (!file.isFile()) {
915                                        newFile = true;
916                                        try {
917                                                file.createNewFile();
918                                        } catch (IOException e) {
919                                                throw new RuntimeException(e);
920                                        }
921                                }
922
923                                if (!file.isFile())
924                                        throw new IllegalStateException("Could not create file (permissions?!): " + file);
925
926                                // A complete sync run might take very long. Therefore, we better update our local meta-data
927                                // *immediately* before beginning the sync of this file and before detecting a collision.
928                                // Furthermore, maybe the file is new and there's no meta-data, yet, hence we must do this anyway.
929                                final RepoFileDAO repoFileDAO = transaction.getDAO(RepoFileDAO.class);
930                                new LocalRepoSync(transaction).sync(file, new NullProgressMonitor());
931
932                                deleteTempChunkFilesWithoutDTOFile(getOffset2TempChunkFileWithDTOFile(file).values());
933
934                                final RepoFile repoFile = repoFileDAO.getRepoFile(localRoot, file);
935                                if (repoFile == null)
936                                        throw new IllegalStateException("LocalRepoSync.sync(...) did not create the RepoFile for file: " + file);
937
938                                if (!(repoFile instanceof NormalFile))
939                                        throw new IllegalStateException("LocalRepoSync.sync(...) created an instance of " + repoFile.getClass().getName() + " instead  of a NormalFile for file: " + file);
940
941                                final NormalFile normalFile = (NormalFile) repoFile;
942
943                                if (!newFile && !normalFile.isInProgress())
944                                        detectAndHandleFileCollision(transaction, clientRepositoryId, file, normalFile);
945
946                                normalFile.setLastSyncFromRepositoryId(clientRepositoryId);
947                                normalFile.setInProgress(true);
948                        } finally {
949                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile);
950                        }
951                        transaction.commit();
952                } finally {
953                        transaction.rollbackIfActive();
954                }
955        }
956
957        private boolean isSymlink(final File file) {
958                return Files.isSymbolicLink(file.toPath());
959        }
960
961        /**
962         * Detect if the file to be copied has been modified locally (or copied from another repository) after the last
963         * sync from the repository identified by {@code fromRepositoryId}.
964         * <p>
965         * If there is a collision - i.e. the destination file has been modified, too - then the destination file is moved
966         * away by renaming it. The name to which it is renamed is created by {@link IOUtil#createCollisionFile(File)}.
967         * Afterwards the file is copied back to its original name.
968         * <p>
969         * The reason for renaming it first (instead of directly copying it) is that there might be open file handles.
970         * In GNU/Linux, the open file handles stay open and thus are then connected to the renamed file, thus continuing
971         * to modify the file which was moved away. In Windows, the renaming likely fails and we abort with an exception.
972         * In both cases, we do our best to avoid both processes from writing to the same file simultaneously without locking
973         * it.
974         * <p>
975         * In the future (this is NOT YET IMPLEMENTED), we might lock it in {@link #beginPutFile(String)} and
976         * keep the lock until {@link #endPutFile(String, Date, long, String)} or a timeout occurs - and refresh the lock
977         * (i.e. postpone the timeout) with every {@link #putFileData(String, long, byte[])}. The reason for this
978         * quite complicated strategy is that we cannot guarantee that the {@link #endPutFile(String, Date, long, String)}
979         * is ever invoked (the client might crash inbetween). We don't want a locked file to linger forever.
980         *
981         * @param transaction the DB transaction. Must not be <code>null</code>.
982         * @param fromRepositoryId the ID of the source repository from which the file is about to be copied. Must not be <code>null</code>.
983         * @param file the file that is to be copied (i.e. overwritten). Must not be <code>null</code>.
984         * @param normalFileOrSymlink the DB entity corresponding to {@code file}. Must not be <code>null</code>.
985         */
986        private void detectAndHandleFileCollision(LocalRepoTransaction transaction, UUID fromRepositoryId, File file, RepoFile normalFileOrSymlink) {
987                if (detectFileCollision(transaction, fromRepositoryId, file, normalFileOrSymlink)) {
988                        final File collisionFile = IOUtil.createCollisionFile(file);
989                        file.renameTo(collisionFile);
990                        if (file.exists())
991                                throw new IllegalStateException("Could not rename file to resolve collision: " + file);
992
993                        try {
994                                Files.copy(collisionFile.toPath(), file.toPath(), StandardCopyOption.COPY_ATTRIBUTES);
995                        } catch (IOException e) {
996                                throw new RuntimeException(e);
997                        }
998
999                        new LocalRepoSync(transaction).sync(collisionFile, new NullProgressMonitor()); // TODO sub-progress-monitor!
1000                }
1001        }
1002
1003        private boolean detectFileCollisionRecursively(LocalRepoTransaction transaction, UUID fromRepositoryId, File fileOrDirectory) {
1004                assertNotNull("transaction", transaction);
1005                assertNotNull("fromRepositoryId", fromRepositoryId);
1006                assertNotNull("fileOrDirectory", fileOrDirectory);
1007
1008                // we handle symlinks before invoking exists() below, because this method and most other File methods resolve symlinks!
1009                if (Files.isSymbolicLink(fileOrDirectory.toPath())) {
1010                        RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fileOrDirectory);
1011                        if (!(repoFile instanceof Symlink))
1012                                return true; // We had a change after the last local sync (symlink => directory or normal file)!
1013
1014                        return detectFileCollision(transaction, fromRepositoryId, fileOrDirectory, repoFile);
1015                }
1016
1017                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?!
1018                        return false;
1019                }
1020
1021                if (fileOrDirectory.isFile()) {
1022                        RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), fileOrDirectory);
1023                        if (!(repoFile instanceof NormalFile))
1024                                return true; // We had a change after the last local sync (normal file => directory or symlink)!
1025
1026                        return detectFileCollision(transaction, fromRepositoryId, fileOrDirectory, repoFile);
1027                }
1028
1029                File[] children = fileOrDirectory.listFiles();
1030                if (children == null)
1031                        throw new IllegalStateException("listFiles() of directory returned null: " + fileOrDirectory);
1032
1033                for (File child : children) {
1034                        if (detectFileCollisionRecursively(transaction, fromRepositoryId, child))
1035                                return true;
1036                }
1037
1038                return false;
1039        }
1040
1041        /**
1042         * Detect if the file to be copied or deleted has been modified locally (or copied from another repository) after the last
1043         * sync from the repository identified by {@code fromRepositoryId}.
1044         * @param transaction
1045         * @param fromRepositoryId
1046         * @param file
1047         * @param normalFileOrSymlink
1048         * @return <code>true</code>, if there is a collision; <code>false</code>, if there is none.
1049         */
1050        private boolean detectFileCollision(LocalRepoTransaction transaction, UUID fromRepositoryId, File file, RepoFile normalFileOrSymlink) {
1051                assertNotNull("transaction", transaction);
1052                assertNotNull("fromRepositoryId", fromRepositoryId);
1053                assertNotNull("file", file);
1054                assertNotNull("normalFileOrSymlink", normalFileOrSymlink);
1055
1056                if (!file.exists()) {
1057                        logger.debug("detectFileCollision: path='{}': return false, because destination file does not exist.", normalFileOrSymlink.getPath());
1058                        return false;
1059                }
1060
1061                RemoteRepository fromRemoteRepository = transaction.getDAO(RemoteRepositoryDAO.class).getRemoteRepositoryOrFail(fromRepositoryId);
1062                long lastSyncFromRemoteRepositoryLocalRevision = fromRemoteRepository.getLocalRevision();
1063                if (normalFileOrSymlink.getLocalRevision() <= lastSyncFromRemoteRepositoryLocalRevision) {
1064                        logger.debug("detectFileCollision: path='{}': return false, because: normalFileOrSymlink.localRevision <= lastSyncFromRemoteRepositoryLocalRevision :: {} <= {}", normalFileOrSymlink.getPath(), normalFileOrSymlink.getLocalRevision(), lastSyncFromRemoteRepositoryLocalRevision);
1065                        return false;
1066                }
1067
1068                // The file was transferred from the same repository before and was thus not changed locally nor in another repo.
1069                // This can only happen, if the sync was interrupted (otherwise the check for the localRevision above
1070                // would have already caused this method to abort).
1071                if (fromRepositoryId.equals(normalFileOrSymlink.getLastSyncFromRepositoryId())) {
1072                        logger.debug("detectFileCollision: path='{}': return false, because: fromRepositoryId == normalFileOrSymlink.lastSyncFromRepositoryId :: fromRepositoryId='{}'", normalFileOrSymlink.getPath(), fromRemoteRepository);
1073                        return false;
1074                }
1075
1076                logger.debug("detectFileCollision: path='{}': return true! fromRepositoryId='{}' normalFileOrSymlink.localRevision={} lastSyncFromRemoteRepositoryLocalRevision={} normalFileOrSymlink.lastSyncFromRepositoryId='{}'",
1077                                normalFileOrSymlink.getPath(), fromRemoteRepository, normalFileOrSymlink.getLocalRevision(), lastSyncFromRemoteRepositoryLocalRevision, normalFileOrSymlink.getLastSyncFromRepositoryId());
1078                return true;
1079        }
1080
1081        @Override
1082        public void putFileData(String path, final long offset, final byte[] fileData) {
1083                path = prefixPath(path);
1084                final File file = getFile(path);
1085                final File parentFile = file.getParentFile();
1086                final File localRoot = getLocalRepoManager().getLocalRoot();
1087                final LocalRepoTransaction transaction = getLocalRepoManager().beginReadTransaction(); // It writes into the file system, but it only reads from the DB.
1088                try {
1089                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile);
1090                        try {
1091                                RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(localRoot, file);
1092                                if (repoFile == null)
1093                                        throw new IllegalStateException("No RepoFile found for file: " + file);
1094
1095                                if (!(repoFile instanceof NormalFile))
1096                                        throw new IllegalStateException("RepoFile is not an instance of NormalFile for file: " + file);
1097
1098                                NormalFile normalFile = (NormalFile) repoFile;
1099                                if (!normalFile.isInProgress())
1100                                        throw new IllegalStateException(String.format("NormalFile.inProgress == false! beginFile(...) not called?! repoFile=%s file=%s",
1101                                                        repoFile, file));
1102
1103                                final FileWriteStrategy fileWriteStrategy = getFileWriteStrategy(file);
1104                                logger.debug("putFileData: fileWriteStrategy={}", fileWriteStrategy);
1105                                switch (fileWriteStrategy) {
1106                                        case directDuringTransfer:
1107                                                writeFileDataToDestFile(file, offset, fileData);
1108                                                break;
1109                                        case directAfterTransfer:
1110                                        case replaceAfterTransfer:
1111                                                writeFileDataToTempChunkFile(file, offset, fileData);
1112                                                break;
1113                                        default:
1114                                                throw new IllegalStateException("Unknown fileWriteStrategy: " + fileWriteStrategy);
1115                                }
1116                        } finally {
1117                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile);
1118                        }
1119                        transaction.commit();
1120                } finally {
1121                        transaction.rollbackIfActive();
1122                }
1123        }
1124
1125        private void writeTempChunkFileToDestFile(final File destFile, final File tempChunkFile, final TempChunkFileDTO tempChunkFileDTO) {
1126                assertNotNull("destFile", destFile);
1127                assertNotNull("tempChunkFile", tempChunkFile);
1128                assertNotNull("tempChunkFileDTO", tempChunkFileDTO);
1129                final long offset = assertNotNull("tempChunkFileDTO.fileChunkDTO", tempChunkFileDTO.getFileChunkDTO()).getOffset();
1130                final byte[] fileData = new byte[(int) tempChunkFile.length()];
1131                try {
1132                        final InputStream in = new FileInputStream(tempChunkFile);
1133                        try {
1134                                int off = 0;
1135                                while (off < fileData.length) {
1136                                        final int bytesRead = in.read(fileData, off, fileData.length - off);
1137                                        if (bytesRead > 0) {
1138                                                off += bytesRead;
1139                                        }
1140                                        else if (bytesRead < 0) {
1141                                                throw new IllegalStateException("InputStream ended before expected file length!");
1142                                        }
1143                                }
1144                                if (off > fileData.length || in.read() != -1)
1145                                        throw new IllegalStateException("InputStream contained more data than expected file length!");
1146                        } finally {
1147                                in.close();
1148                        }
1149                } catch (IOException e) {
1150                        throw new RuntimeException(e);
1151                }
1152
1153                final String sha1FromDTOFile = tempChunkFileDTO.getFileChunkDTO().getSha1();
1154                final String sha1FromFileData = sha1(fileData);
1155
1156                logger.trace("writeTempChunkFileToDestFile: Read {} bytes with SHA1 '{}' from '{}'.", fileData.length, sha1FromFileData, tempChunkFile.getAbsolutePath());
1157
1158                if (!sha1FromFileData.equals(sha1FromDTOFile))
1159                        throw new IllegalStateException("SHA1 mismatch! Corrupt temporary chunk file or corresponding DTO file: " + tempChunkFile.getAbsolutePath());
1160
1161                writeFileDataToDestFile(destFile, offset, fileData);
1162        }
1163
1164        private void writeFileDataToDestFile(final File destFile, final long offset, final byte[] fileData) {
1165                assertNotNull("destFile", destFile);
1166                assertNotNull("fileData", fileData);
1167                try {
1168                        final RandomAccessFile raf = new RandomAccessFile(destFile, "rw");
1169                        try {
1170                                raf.seek(offset);
1171                                raf.write(fileData);
1172                        } finally {
1173                                raf.close();
1174                        }
1175                        logger.trace("writeFileDataToDestFile: Wrote {} bytes at offset {} to '{}'.", fileData.length, offset, destFile.getAbsolutePath());
1176                } catch (IOException e) {
1177                        throw new RuntimeException(e);
1178                }
1179        }
1180
1181        private void writeFileDataToTempChunkFile(final File destFile, final long offset, final byte[] fileData) {
1182                assertNotNull("destFile", destFile);
1183                assertNotNull("fileData", fileData);
1184                try {
1185                        final File tempChunkFile = createTempChunkFile(destFile, offset);
1186                        final File tempChunkFileDTOFile = getTempChunkFileDTOFile(tempChunkFile);
1187
1188                        // Delete the meta-data-file, in case we overwrite an older temp-chunk-file. This way it
1189                        // is guaranteed, that if the meta-data-file exists, it is consistent with either
1190                        // the temp-chunk-file or the chunk was already written into the final destination.
1191                        deleteOrFail(tempChunkFileDTOFile);
1192
1193                        final FileOutputStream out = new FileOutputStream(tempChunkFile);
1194                        try {
1195                                out.write(fileData);
1196                        } finally {
1197                                out.close();
1198                        }
1199                        final String sha1 = sha1(fileData);
1200                        logger.trace("writeFileDataToTempChunkFile: Wrote {} bytes with SHA1 '{}' to '{}'.", fileData.length, sha1, tempChunkFile.getAbsolutePath());
1201                        final TempChunkFileDTO tempChunkFileDTO = createTempChunkFileDTO(offset, tempChunkFile, sha1);
1202                        new TempChunkFileDTOIO().serialize(tempChunkFileDTO, tempChunkFileDTOFile);
1203                } catch (IOException e) {
1204                        throw new RuntimeException(e);
1205                }
1206        }
1207
1208        private void deleteTempChunkFilesWithoutDTOFile(Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles) {
1209                for (final TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) {
1210                        final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile();
1211                        if (tempChunkFileDTOFile == null || !tempChunkFileDTOFile.exists()) {
1212                                File tempChunkFile = tempChunkFileWithDTOFile.getTempChunkFile();
1213                                logger.warn("deleteTempChunkFilesWithoutDTOFile: No DTO-file for temporary chunk-file '{}'! DELETING this temporary file!", tempChunkFile.getAbsolutePath());
1214                                deleteOrFail(tempChunkFile);
1215                                continue;
1216                        }
1217                }
1218        }
1219
1220        private Map<Long, TempChunkFileWithDTOFile> getOffset2TempChunkFileWithDTOFile(final File destFile) {
1221                final File[] tempFiles = getTempDir(destFile).listFiles();
1222                if (tempFiles == null)
1223                        return Collections.emptyMap();
1224
1225                final String destFileName = destFile.getName();
1226                final Map<Long, TempChunkFileWithDTOFile> result = new TreeMap<Long, TempChunkFileWithDTOFile>();
1227                for (final File tempFile : tempFiles) {
1228                        String tempFileName = tempFile.getName();
1229                        if (!tempFileName.startsWith(TEMP_CHUNK_FILE_PREFIX))
1230                                continue;
1231
1232                        final boolean dtoFile;
1233                        if (tempFileName.endsWith(TEMP_CHUNK_FILE_DTO_FILE_SUFFIX)) {
1234                                dtoFile = true;
1235                                tempFileName = tempFileName.substring(0, tempFileName.length() - TEMP_CHUNK_FILE_DTO_FILE_SUFFIX.length());
1236                        }
1237                        else
1238                                dtoFile = false;
1239
1240                        final int lastUnderscoreIndex = tempFileName.lastIndexOf('_');
1241                        if (lastUnderscoreIndex < 0)
1242                                throw new IllegalStateException("lastUnderscoreIndex < 0 :: tempFileName='" + tempFileName + '\'');
1243
1244                        final String tempFileDestFileName = tempFileName.substring(TEMP_CHUNK_FILE_PREFIX.length(), lastUnderscoreIndex);
1245                        if (!destFileName.equals(tempFileDestFileName))
1246                                continue;
1247
1248                        final String offsetStr = tempFileName.substring(lastUnderscoreIndex + 1);
1249                        final Long offset = Long.valueOf(offsetStr, 36);
1250                        TempChunkFileWithDTOFile tempChunkFileWithDTOFile = result.get(offset);
1251                        if (tempChunkFileWithDTOFile == null) {
1252                                tempChunkFileWithDTOFile = new TempChunkFileWithDTOFile();
1253                                result.put(offset, tempChunkFileWithDTOFile);
1254                        }
1255                        if (dtoFile)
1256                                tempChunkFileWithDTOFile.setTempChunkFileDTOFile(tempFile);
1257                        else
1258                                tempChunkFileWithDTOFile.setTempChunkFile(tempFile);
1259                }
1260                return Collections.unmodifiableMap(result);
1261        }
1262
1263        private File getTempChunkFileDTOFile(File file) {
1264                return new File(file.getParentFile(), file.getName() + TEMP_CHUNK_FILE_DTO_FILE_SUFFIX);
1265        }
1266
1267        private String sha1(byte[] data) {
1268                assertNotNull("data", data);
1269                try {
1270                        byte[] hash = HashUtil.hash(HashUtil.HASH_ALGORITHM_SHA, new ByteArrayInputStream(data));
1271                        return HashUtil.encodeHexStr(hash);
1272                } catch (NoSuchAlgorithmException e) {
1273                        throw new RuntimeException(e);
1274                } catch (IOException e) {
1275                        throw new RuntimeException(e);
1276                }
1277        }
1278
1279        /**
1280         * Create the temporary file for the given {@code destFile} and {@code offset}.
1281         * <p>
1282         * The returned file is created, if it does not yet exist; but it is <i>not</i> overwritten,
1283         * if it already exists.
1284         * <p>
1285         * The {@linkplain #getTempDir(File) temporary directory} in which the temporary file is located
1286         * is created, if necessary. In order to prevent collisions with code trying to delete the empty
1287         * temporary directory, this method and the corresponding {@link #deleteTempDirIfEmpty(File)} are
1288         * both synchronized.
1289         * @param destFile the destination file for which to resolve and create the temporary file.
1290         * Must not be <code>null</code>.
1291         * @param offset the offset (inside the final destination file and the source file) of the block to
1292         * be temporarily stored in the temporary file created by this method. The temporary file will hold
1293         * solely this block (thus the offset in the temporary file is 0).
1294         * @return the temporary file. Never <code>null</code>. The file is already created in the file system
1295         * (empty), if it did not yet exist.
1296         */
1297        private synchronized File createTempChunkFile(final File destFile, final long offset) {
1298                final File tempDir = getTempDir(destFile);
1299                tempDir.mkdir();
1300                if (!tempDir.isDirectory())
1301                        throw new IllegalStateException("Creating the directory failed (it does not exist after mkdir): " + tempDir.getAbsolutePath());
1302
1303                final File tempFile = new File(tempDir, String.format("%s%s_%s",
1304                                TEMP_CHUNK_FILE_PREFIX, destFile.getName(), Long.toString(offset, 36)));
1305                try {
1306                        tempFile.createNewFile();
1307                } catch (IOException e) {
1308                        throw new RuntimeException(e);
1309                }
1310                return tempFile;
1311        }
1312
1313        /**
1314         * Deletes the {@linkplain #getTempDir(File) temporary directory} for the given {@code destFile},
1315         * if this directory is empty.
1316         * <p>
1317         * This method is synchronized to prevent it from colliding with {@link #createTempChunkFile(File, long)}
1318         * which first creates the temporary directory and then the file in it. Without synchronisation, the
1319         * newly created directory might be deleted by this method, before the temporary file in it is created.
1320         * @param destFile the destination file for which to resolve and delete the temporary directory.
1321         * Must not be <code>null</code>.
1322         */
1323        private synchronized void deleteTempDirIfEmpty(final File destFile) {
1324                final File tempDir = getTempDir(destFile);
1325                tempDir.delete(); // deletes only empty directories ;-)
1326        }
1327
1328        private File getTempDir(final File destFile) {
1329                assertNotNull("destFile", destFile);
1330                final File parentDir = destFile.getParentFile();
1331                return new File(parentDir, LocalRepoManager.TEMP_DIR_NAME);
1332        }
1333
1334        private final Map<File, FileWriteStrategy> file2FileWriteStrategy = new WeakHashMap<>();
1335
1336        private FileWriteStrategy getFileWriteStrategy(File file) {
1337                assertNotNull("file", file);
1338                synchronized (file2FileWriteStrategy) {
1339                        FileWriteStrategy fileWriteStrategy = file2FileWriteStrategy.get(file);
1340                        if (fileWriteStrategy == null) {
1341                                fileWriteStrategy = Config.getInstanceForFile(file).getPropertyAsEnum(FileWriteStrategy.CONFIG_KEY, FileWriteStrategy.CONFIG_DEFAULT_VALUE);
1342                                file2FileWriteStrategy.put(file, fileWriteStrategy);
1343                        }
1344                        return fileWriteStrategy;
1345                }
1346        }
1347
1348        @Override
1349        public void endPutFile(String path, final Date lastModified, final long length, String sha1) {
1350                path = prefixPath(path);
1351                assertNotNull("lastModified", lastModified);
1352                assertNotNull("sha1", sha1);
1353                final File file = getFile(path);
1354                final File parentFile = file.getParentFile();
1355                final UUID clientRepositoryId = getClientRepositoryIdOrFail();
1356                final LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
1357                try {
1358                        ParentFileLastModifiedManager.getInstance().backupParentFileLastModified(parentFile);
1359                        try {
1360                                final RepoFile repoFile = transaction.getDAO(RepoFileDAO.class).getRepoFile(getLocalRepoManager().getLocalRoot(), file);
1361                                if (!(repoFile instanceof NormalFile)) {
1362                                        throw new IllegalStateException(String.format("RepoFile is not an instance of NormalFile! repoFile=%s file=%s",
1363                                                        repoFile, file));
1364                                }
1365
1366                                final NormalFile normalFile = (NormalFile) repoFile;
1367                                if (!normalFile.isInProgress())
1368                                        throw new IllegalStateException(String.format("NormalFile.inProgress == false! beginFile(...) not called?! repoFile=%s file=%s",
1369                                                        repoFile, file));
1370
1371                                final FileWriteStrategy fileWriteStrategy = getFileWriteStrategy(file);
1372                                logger.debug("endPutFile: fileWriteStrategy={}", fileWriteStrategy);
1373
1374                                final File destFile = (fileWriteStrategy == FileWriteStrategy.replaceAfterTransfer
1375                                                ? new File(file.getParentFile(), LocalRepoManager.TEMP_NEW_FILE_PREFIX + file.getName()) : file);
1376
1377                                final InputStream fileIn;
1378                                if (destFile != file) {
1379                                        try {
1380                                                fileIn = new FileInputStream(file);
1381                                                destFile.createNewFile();
1382                                        } catch (IOException e) {
1383                                                throw new RuntimeException(e);
1384                                        }
1385                                }
1386                                else
1387                                        fileIn = null;
1388
1389                                final TempChunkFileDTOIO tempChunkFileDTOIO = new TempChunkFileDTOIO();
1390                                long destFileWriteOffset = 0;
1391                                // tempChunkFileWithDTOFiles are sorted by offset (ascending)
1392                                final Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles = getOffset2TempChunkFileWithDTOFile(file).values();
1393                                for (final TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) {
1394                                        final File tempChunkFile = tempChunkFileWithDTOFile.getTempChunkFile(); // tempChunkFile may be null!!!
1395                                        final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile();
1396                                        if (tempChunkFileDTOFile == null)
1397                                                throw new IllegalStateException("No meta-data (tempChunkFileDTOFile) for file: " + (tempChunkFile == null ? null : tempChunkFile.getAbsolutePath()));
1398
1399                                        final TempChunkFileDTO tempChunkFileDTO = tempChunkFileDTOIO.deserialize(tempChunkFileDTOFile);
1400                                        final long offset = assertNotNull("tempChunkFileDTO.fileChunkDTO", tempChunkFileDTO.getFileChunkDTO()).getOffset();
1401
1402                                        if (fileIn != null) {
1403                                                // The following might fail, if *file* was truncated during the transfer. In this case,
1404                                                // throwing an exception now is probably the best choice as the next sync run will
1405                                                // continue cleanly.
1406                                                writeFileDataToDestFile(destFile, destFileWriteOffset, fileIn, offset - destFileWriteOffset);
1407                                                final long tempChunkFileLength = tempChunkFileDTO.getFileChunkDTO().getLength();
1408                                                skipOrFail(fileIn, tempChunkFileLength); // skipping beyond the EOF is supported by the FileInputStream according to Javadoc.
1409                                                destFileWriteOffset = offset + tempChunkFileLength;
1410                                        }
1411
1412                                        if (tempChunkFile != null && tempChunkFile.exists()) {
1413                                                writeTempChunkFileToDestFile(destFile, tempChunkFile, tempChunkFileDTO);
1414                                                deleteOrFail(tempChunkFile);
1415                                        }
1416                                }
1417
1418                                if (fileIn != null && destFileWriteOffset < length)
1419                                        writeFileDataToDestFile(destFile, destFileWriteOffset, fileIn, length - destFileWriteOffset);
1420
1421                                try {
1422                                        final RandomAccessFile raf = new RandomAccessFile(destFile, "rw");
1423                                        try {
1424                                                raf.setLength(length);
1425                                        } finally {
1426                                                raf.close();
1427                                        }
1428                                } catch (IOException e) {
1429                                        throw new RuntimeException(e);
1430                                }
1431
1432                                if (destFile != file) {
1433                                        deleteOrFail(file);
1434                                        destFile.renameTo(file);
1435                                        if (!file.exists())
1436                                                throw new IllegalStateException(String.format("Renaming the file from '%s' to '%s' failed: The destination file does not exist.", destFile.getAbsolutePath(), file.getAbsolutePath()));
1437
1438                                        if (destFile.exists())
1439                                                throw new IllegalStateException(String.format("Renaming the file from '%s' to '%s' failed: The source file still exists.", destFile.getAbsolutePath(), file.getAbsolutePath()));
1440                                }
1441
1442                                deleteTempChunkFiles(tempChunkFileWithDTOFiles);
1443                                deleteTempDirIfEmpty(file);
1444
1445                                LocalRepoSync localRepoSync = new LocalRepoSync(transaction);
1446                                file.setLastModified(lastModified.getTime());
1447                                localRepoSync.updateRepoFile(normalFile, file, new NullProgressMonitor());
1448                                normalFile.setLastSyncFromRepositoryId(clientRepositoryId);
1449                                normalFile.setInProgress(false);
1450
1451                                logger.trace("endPutFile: Committing: sha1='{}' file='{}'", normalFile.getSha1(), file);
1452                                if (!sha1.equals(normalFile.getSha1())) {
1453                                        logger.warn("endPutFile: File was modified during transport (either on source or destination side): expectedSha1='{}' foundSha1='{}' file='{}'",
1454                                                        sha1, normalFile.getSha1(), file);
1455                                }
1456
1457                        } finally {
1458                                ParentFileLastModifiedManager.getInstance().restoreParentFileLastModified(parentFile);
1459                        }
1460                        transaction.commit();
1461                } finally {
1462                        transaction.rollbackIfActive();
1463                }
1464        }
1465
1466        private void deleteTempChunkFiles(final Collection<TempChunkFileWithDTOFile> tempChunkFileWithDTOFiles) {
1467                for (final TempChunkFileWithDTOFile tempChunkFileWithDTOFile : tempChunkFileWithDTOFiles) {
1468                        final File tempChunkFile = tempChunkFileWithDTOFile.getTempChunkFile(); // tempChunkFile may be null!!!
1469                        final File tempChunkFileDTOFile = tempChunkFileWithDTOFile.getTempChunkFileDTOFile();
1470
1471                        if (tempChunkFile != null && tempChunkFile.exists())
1472                                deleteOrFail(tempChunkFile);
1473
1474                        if (tempChunkFileDTOFile != null && tempChunkFileDTOFile.exists())
1475                                deleteOrFail(tempChunkFileDTOFile);
1476                }
1477        }
1478
1479        private void deleteOrFail(File file) {
1480                file.delete();
1481                if (isSymlink(file) || file.exists())
1482                        throw new IllegalStateException("Could not delete file (it still exists after deletion): " + file);
1483        }
1484
1485        /**
1486         * Skip the given {@code length} number of bytes.
1487         * <p>
1488         * Because {@link InputStream#skip(long)} and {@link FileInputStream#skip(long)} are both documented to skip
1489         * over less than the requested number of bytes "for a number of reasons", this method invokes the underlying
1490         * skip(...) method multiple times until either EOF is reached or the requested number of bytes was skipped.
1491         * In case of EOF, an
1492         * @param in the {@link InputStream} to be skipped. Must not be <code>null</code>.
1493         * @param length the number of bytes to be skipped. Must not be negative (i.e. <code>length &gt;= 0</code>).
1494         */
1495        private void skipOrFail(InputStream in, long length) {
1496                assertNotNull("in", in);
1497                if (length < 0)
1498                        throw new IllegalArgumentException("length < 0");
1499
1500                long skipped = 0;
1501                int skippedNowWas0Counter = 0;
1502                while (skipped < length) {
1503                        final long toSkip = length - skipped;
1504                        try {
1505                                final long skippedNow = in.skip(toSkip);
1506                                if (skippedNow < 0)
1507                                        throw new IOException("in.skip(" + toSkip + ") returned " + skippedNow);
1508
1509                                if (skippedNow == 0) {
1510                                        if (++skippedNowWas0Counter >= 5) {
1511                                                throw new IOException(String.format(
1512                                                                "Could not skip %s consecutive times!", skippedNowWas0Counter));
1513                                        }
1514                                }
1515                                else
1516                                        skippedNowWas0Counter = 0;
1517
1518                                skipped += skippedNow;
1519                        } catch (IOException e) {
1520                                throw new RuntimeException(e);
1521                        }
1522                }
1523        }
1524
1525        private void writeFileDataToDestFile(final File destFile, final long offset, final InputStream in, final long length) {
1526                assertNotNull("destFile", destFile);
1527                assertNotNull("in", in);
1528                if (offset < 0)
1529                        throw new IllegalArgumentException("offset < 0");
1530
1531                if (length == 0)
1532                        return;
1533
1534                if (length < 0)
1535                        throw new IllegalArgumentException("length < 0");
1536
1537                long lengthDone = 0;
1538
1539                try {
1540                        final RandomAccessFile raf = new RandomAccessFile(destFile, "rw");
1541                        try {
1542                                raf.seek(offset);
1543
1544                                final byte[] buf = new byte[200 * 1024];
1545
1546                                while (lengthDone < length) {
1547                                        final long len = Math.min(length - lengthDone, buf.length);
1548                                        final int bytesRead = in.read(buf, 0, (int)len);
1549                                        if (bytesRead > 0) {
1550                                                raf.write(buf, 0, bytesRead);
1551                                                lengthDone += bytesRead;
1552                                        }
1553                                        else if (bytesRead < 0)
1554                                                throw new IOException("Premature end of stream!");
1555                                }
1556                        } finally {
1557                                raf.close();
1558                        }
1559                } catch (IOException e) {
1560                        throw new RuntimeException(e);
1561                }
1562        }
1563
1564        @Override
1565        public void endSyncFromRepository() {
1566                UUID clientRepositoryId = getClientRepositoryIdOrFail();
1567                LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
1568                try {
1569                        PersistenceManager pm = ((co.codewizards.cloudstore.local.LocalRepoTransactionImpl)transaction).getPersistenceManager();
1570                        RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class);
1571                        LastSyncToRemoteRepoDAO lastSyncToRemoteRepoDAO = transaction.getDAO(LastSyncToRemoteRepoDAO.class);
1572                        ModificationDAO modificationDAO = transaction.getDAO(ModificationDAO.class);
1573
1574                        RemoteRepository toRemoteRepository = remoteRepositoryDAO.getRemoteRepositoryOrFail(clientRepositoryId);
1575
1576                        LastSyncToRemoteRepo lastSyncToRemoteRepo = lastSyncToRemoteRepoDAO.getLastSyncToRemoteRepoOrFail(toRemoteRepository);
1577                        if (lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress() < 0)
1578                                throw new IllegalStateException(String.format("lastSyncToRemoteRepo.localRepositoryRevisionInProgress < 0 :: There is no sync in progress for the RemoteRepository with entityID=%s", clientRepositoryId));
1579
1580                        lastSyncToRemoteRepo.setLocalRepositoryRevisionSynced(lastSyncToRemoteRepo.getLocalRepositoryRevisionInProgress());
1581                        lastSyncToRemoteRepo.setLocalRepositoryRevisionInProgress(-1);
1582
1583                        pm.flush(); // prevent problems caused by batching, deletion and foreign keys
1584                        Collection<Modification> modifications = modificationDAO.getModificationsBeforeOrEqual(
1585                                        toRemoteRepository, lastSyncToRemoteRepo.getLocalRepositoryRevisionSynced());
1586                        modificationDAO.deletePersistentAll(modifications);
1587                        pm.flush();
1588
1589                        transaction.commit();
1590                } finally {
1591                        transaction.rollbackIfActive();
1592                }
1593        }
1594
1595        @Override
1596        public void endSyncToRepository(long fromLocalRevision) {
1597                UUID clientRepositoryId = getClientRepositoryIdOrFail();
1598                LocalRepoTransaction transaction = getLocalRepoManager().beginWriteTransaction();
1599                try {
1600                        RemoteRepositoryDAO remoteRepositoryDAO = transaction.getDAO(RemoteRepositoryDAO.class);
1601                        RemoteRepository remoteRepository = remoteRepositoryDAO.getRemoteRepositoryOrFail(clientRepositoryId);
1602                        remoteRepository.setRevision(fromLocalRevision);
1603
1604                        transaction.commit();
1605                } finally {
1606                        transaction.rollbackIfActive();
1607                }
1608        }
1609
1610}