001package co.codewizards.cloudstore.local;
002
003import static co.codewizards.cloudstore.core.io.StreamUtil.*;
004import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*;
005import static java.util.Objects.*;
006
007import java.io.IOException;
008import java.io.InputStream;
009import java.security.MessageDigest;
010import java.security.NoSuchAlgorithmException;
011import java.util.ArrayList;
012import java.util.Collection;
013import java.util.Collections;
014import java.util.Date;
015import java.util.HashMap;
016import java.util.HashSet;
017import java.util.List;
018import java.util.Map;
019import java.util.Set;
020
021import javax.jdo.PersistenceManager;
022
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import co.codewizards.cloudstore.core.dto.FileChunkDto;
027import co.codewizards.cloudstore.core.ignore.IgnoreRuleManagerImpl;
028import co.codewizards.cloudstore.core.oio.File;
029import co.codewizards.cloudstore.core.progress.ProgressMonitor;
030import co.codewizards.cloudstore.core.progress.SubProgressMonitor;
031import co.codewizards.cloudstore.core.repo.local.LocalRepoTransaction;
032import co.codewizards.cloudstore.core.util.HashUtil;
033import co.codewizards.cloudstore.local.persistence.CopyModification;
034import co.codewizards.cloudstore.local.persistence.DeleteModification;
035import co.codewizards.cloudstore.local.persistence.DeleteModificationDao;
036import co.codewizards.cloudstore.local.persistence.Directory;
037import co.codewizards.cloudstore.local.persistence.FileChunk;
038import co.codewizards.cloudstore.local.persistence.ModificationDao;
039import co.codewizards.cloudstore.local.persistence.NormalFile;
040import co.codewizards.cloudstore.local.persistence.NormalFileDao;
041import co.codewizards.cloudstore.local.persistence.RemoteRepository;
042import co.codewizards.cloudstore.local.persistence.RemoteRepositoryDao;
043import co.codewizards.cloudstore.local.persistence.RepoFile;
044import co.codewizards.cloudstore.local.persistence.RepoFileDao;
045import co.codewizards.cloudstore.local.persistence.Symlink;
046
047public class LocalRepoSync {
048
049        private static final Logger logger = LoggerFactory.getLogger(LocalRepoSync.class);
050
051        protected final LocalRepoTransaction transaction;
052        protected final File localRoot;
053        protected final RepoFileDao repoFileDao;
054        protected final NormalFileDao normalFileDao;
055        protected final RemoteRepositoryDao remoteRepositoryDao;
056        protected final ModificationDao modificationDao;
057        protected final DeleteModificationDao deleteModificationDao;
058        private Collection<RemoteRepository> remoteRepositories;
059        private boolean ignoreRulesEnabled;
060
061        private final Map<String, Set<String>> sha1AndLength2Paths = new HashMap<String, Set<String>>();
062
063        protected LocalRepoSync(final LocalRepoTransaction transaction) {
064                this.transaction = requireNonNull(transaction, "transaction");
065                localRoot = this.transaction.getLocalRepoManager().getLocalRoot();
066                repoFileDao = this.transaction.getDao(RepoFileDao.class);
067                normalFileDao = this.transaction.getDao(NormalFileDao.class);
068                remoteRepositoryDao = this.transaction.getDao(RemoteRepositoryDao.class);
069                modificationDao = this.transaction.getDao(ModificationDao.class);
070                deleteModificationDao = this.transaction.getDao(DeleteModificationDao.class);
071        }
072
073        public static LocalRepoSync create(final LocalRepoTransaction transaction) {
074                return createObject(LocalRepoSync.class, transaction);
075        }
076
077        public void sync(final ProgressMonitor monitor) {
078                sync(null, localRoot, monitor, true);
079        }
080
081        public RepoFile sync(final File file, final ProgressMonitor monitor, final boolean recursiveChildren) {
082                if (!(requireNonNull(file, "file").isAbsolute()))
083                        throw new IllegalArgumentException("file is not absolute: " + file);
084
085                if (localRoot.equals(file)) {
086                        return sync(null, file, monitor, recursiveChildren);
087                }
088
089                monitor.beginTask("Local sync...", 100);
090                try {
091                        final File parentFile = file.getParentFile();
092                        RepoFile parentRepoFile = repoFileDao.getRepoFile(localRoot, parentFile);
093                        if (parentRepoFile == null) {
094                                // If the file does not exist and its RepoFile neither exists, then
095                                // this is in sync already and we can simply leave. This regularly
096                                // happens during the deletion of a directory which is the connection-point
097                                // of a remote-repository. The following re-up-sync then leads us here.
098                                // To speed up things, we simply quit as this is a valid state.
099                                if (!file.isSymbolicLink() && !file.exists() && repoFileDao.getRepoFile(localRoot, file) == null)
100                                        return null;
101
102                                // In the unlikely event, that this is not a valid state, we simply sync all
103                                // and return.
104                                sync(null, localRoot, new SubProgressMonitor(monitor, 99), true);
105                                final RepoFile repoFile = repoFileDao.getRepoFile(localRoot, file);
106                                if (repoFile != null) // if it still does not exist, we run into the re-sync below and this might quickly return null, if that is correct or otherwise sync what's needed.
107                                        return repoFile;
108
109                                parentRepoFile = repoFileDao.getRepoFile(localRoot, parentFile);
110                                if (parentRepoFile == null && parentFile.exists())
111                                        throw new IllegalStateException("RepoFile not found for existing file/dir: " + parentFile.getAbsolutePath());
112                        }
113
114                        monitor.worked(1);
115
116                        return sync(parentRepoFile, file, new SubProgressMonitor(monitor, 99), recursiveChildren);
117                } finally {
118                        monitor.done();
119                }
120        }
121
122        /**
123         * Sync the single given {@code file}.
124         * <p>
125         * If {@code file} is a directory, it recursively syncs all its children.
126         * @param parentRepoFile the parent. May be <code>null</code>, if the file is the repository's root-directory.
127         * For non-root files, this must not be <code>null</code>!
128         * @param file the file to be synced. Must not be <code>null</code>.
129         * @param monitor the progress-monitor. Must not be <code>null</code>.
130         * @param recursiveChildren TODO
131         * @return the {@link RepoFile} corresponding to the given {@code file}. Is <code>null</code>, if the given
132         * {@code file} does not exist; otherwise it is never <code>null</code>.
133         */
134        protected RepoFile sync(final RepoFile parentRepoFile, final File file, final ProgressMonitor monitor, final boolean recursiveChildren) {
135                requireNonNull(file, "file");
136                requireNonNull(monitor, "monitor");
137                monitor.beginTask("Local sync...", 100);
138                try {
139                        RepoFile repoFile = repoFileDao.getRepoFile(localRoot, file);
140
141                        if (parentRepoFile != null) {
142                                boolean ignored = isIgnoreRulesEnabled()
143                                                ? IgnoreRuleManagerImpl.getInstanceForDirectory(file.getParentFile()).isIgnored(file)
144                                                                : false;
145                                if (ignored) {
146                                        if (repoFile != null) {
147                                                deleteRepoFile(repoFile, false);
148                                                repoFile = null;
149                                        }
150                                        return null;
151                                }
152                        }
153
154                        // If the type changed - e.g. from normal file to directory - or if the file was deleted
155                        // we must delete the old instance.
156                        if (repoFile != null && !isRepoFileTypeCorrect(repoFile, file)) {
157                                deleteRepoFile(repoFile, false);
158                                repoFile = null;
159                        }
160
161                        final boolean fileIsSymlink = file.isSymbolicLink();
162                        if (repoFile == null) {
163                                if (!fileIsSymlink && !file.exists())
164                                        return null;
165
166                                repoFile = createRepoFile(parentRepoFile, file, new SubProgressMonitor(monitor, 50));
167                                if (repoFile == null) { // ignoring non-normal files.
168                                        return null;
169                                }
170                        } else if (isModified(repoFile, file))
171                                updateRepoFile(repoFile, file, new SubProgressMonitor(monitor, 50));
172                        else
173                                monitor.worked(50);
174
175                        final Set<String> childNames = new HashSet<String>();
176                        if (!fileIsSymlink) {
177                                final SubProgressMonitor childSubProgressMonitor = new SubProgressMonitor(monitor, 50);
178                                final File[] children = file.listFiles(new FilenameFilterSkipMetaDir());
179                                if (children != null && children.length > 0) {
180                                        childSubProgressMonitor.beginTask("Local sync...", children.length);
181                                        for (final File child : children) {
182                                                childNames.add(child.getName());
183
184                                                if (recursiveChildren)
185                                                        sync(repoFile, child, new SubProgressMonitor(childSubProgressMonitor, 1), recursiveChildren);
186                                        }
187                                }
188                                childSubProgressMonitor.done();
189                        }
190
191                        final Collection<RepoFile> childRepoFiles = repoFileDao.getChildRepoFiles(repoFile);
192                        for (final RepoFile childRepoFile : childRepoFiles) {
193                                if (!childNames.contains(childRepoFile.getName())) {
194                                        deleteRepoFile(childRepoFile);
195                                }
196                        }
197
198                        transaction.flush();
199                        return repoFile;
200                } finally {
201                        monitor.done();
202                }
203        }
204
205        /**
206         * Determines, if the type of the given {@code repoFile} matches the type
207         * of the file in the file system referenced by the given {@code file}.
208         * @param repoFile the {@link RepoFile} currently representing the given {@code file} in the database.
209         * Must not be <code>null</code>.
210         * @param file the file in the file system. Must not be <code>null</code>.
211         * @return <code>true</code>, if both types correspond to each other; <code>false</code> otherwise. If
212         * the file does not exist (anymore) in the file system, <code>false</code> is returned, too.
213         */
214        public boolean isRepoFileTypeCorrect(final RepoFile repoFile, final File file) {
215                requireNonNull(repoFile, "repoFile");
216                requireNonNull(file, "file");
217
218                if (file.isSymbolicLink())
219                        return repoFile instanceof Symlink;
220
221                if (file.isFile())
222                        return repoFile instanceof NormalFile;
223
224                if (file.isDirectory())
225                        return repoFile instanceof Directory;
226
227                return false;
228        }
229
230        public boolean isModified(final RepoFile repoFile, final File file) {
231                final long fileLastModified = file.getLastModifiedNoFollow();
232                if (repoFile.getLastModified().getTime() != fileLastModified) {
233                        if (logger.isDebugEnabled()) {
234                                logger.debug("isModified: repoFile.lastModified != file.lastModified: repoFile.lastModified={} file.lastModified={} file={}",
235                                                repoFile.getLastModified(), new Date(fileLastModified), file);
236                        }
237                        return true;
238                }
239
240                if (file.isSymbolicLink()) {
241                        if (!(repoFile instanceof Symlink))
242                                throw new IllegalArgumentException("repoFile is not an instance of Symlink! file=" + file);
243
244                        final Symlink symlink = (Symlink) repoFile;
245                        String fileSymlinkTarget;
246                        try {
247                                fileSymlinkTarget = file.readSymbolicLinkToPathString();
248                        } catch (final IOException e) {
249                                //as this is already checked as symbolicLink, this should never happen!
250                                throw new IllegalArgumentException(e);
251                        }
252                        return !fileSymlinkTarget.equals(symlink.getTarget());
253                }
254                else if (file.isFile()) {
255                        if (!(repoFile instanceof NormalFile))
256                                throw new IllegalArgumentException("repoFile is not an instance of NormalFile! file=" + file);
257
258                        final NormalFile normalFile = (NormalFile) repoFile;
259                        if (normalFile.getLength() != file.length()) {
260                                if (logger.isDebugEnabled()) {
261                                        logger.debug("isModified: normalFile.length != file.length: repoFile.length={} file.length={} file={}",
262                                                        normalFile.getLength(), file.length(), file);
263                                }
264                                return true;
265                        }
266
267                        if (normalFile.getFileChunks().isEmpty()) // TODO remove this - only needed for downward compatibility!
268                                return true;
269                }
270
271                return false;
272        }
273
274        protected RepoFile createRepoFile(final RepoFile parentRepoFile, final File file, final ProgressMonitor monitor) {
275                if (parentRepoFile == null)
276                        throw new IllegalStateException("Creating the root this way is not possible! Why is it not existing, yet?!???");
277
278                monitor.beginTask("Local sync...", 100);
279                try {
280                        final RepoFile repoFile = _createRepoFile(parentRepoFile, file, new SubProgressMonitor(monitor, 98));
281
282                        if (repoFile instanceof NormalFile)
283                                createCopyModificationsIfPossible((NormalFile)repoFile);
284
285                        monitor.worked(1);
286
287                        return repoFileDao.makePersistent(repoFile);
288                } finally {
289                        monitor.done();
290                }
291        }
292
293        protected RepoFile _createRepoFile(final RepoFile parentRepoFile, final File file, final ProgressMonitor monitor) {
294                monitor.beginTask("Local sync...", 100);
295                try {
296                        RepoFile repoFile;
297                        if (file.isSymbolicLink()) {
298                                final Symlink symlink = (Symlink) (repoFile = createObject(Symlink.class));
299                                try {
300                                        symlink.setTarget(file.readSymbolicLinkToPathString());
301                                } catch (final IOException e) {
302                                        throw new RuntimeException(e);
303                                }
304                        } else if (file.isDirectory()) {
305                                repoFile = createObject(Directory.class);
306                        } else if (file.isFile()) {
307                                final NormalFile normalFile = (NormalFile) (repoFile = createObject(NormalFile.class));
308                                sha(normalFile, file, new SubProgressMonitor(monitor, 99));
309                        } else {
310                                if (file.exists())
311                                        logger.warn("createRepoFile: File exists, but is neither a directory nor a normal file! Skipping: {}", file);
312                                else
313                                        logger.warn("createRepoFile: File does not exist! Skipping: {}", file);
314
315                                return null;
316                        }
317
318                        repoFile.setParent(parentRepoFile);
319                        repoFile.setName(file.getName());
320                        repoFile.setLastModified(new Date(file.getLastModifiedNoFollow()));
321
322                        return repoFile;
323                } finally {
324                        monitor.done();
325                }
326        }
327
328        public void updateRepoFile(final RepoFile repoFile, final File file, final ProgressMonitor monitor) {
329                logger.debug("updateRepoFile: id={} file={}", repoFile.getId(), file);
330                monitor.beginTask("Local sync...", 100);
331                try {
332                        if (file.isSymbolicLink()) {
333                                if (!(repoFile instanceof Symlink))
334                                        throw new IllegalArgumentException("repoFile is not an instance of Symlink! file=" + file);
335
336                                final Symlink symlink = (Symlink) repoFile;
337                                try {
338                                        symlink.setTarget(file.readSymbolicLinkToPathString());
339                                } catch (final IOException e) {
340                                        throw new RuntimeException(e);
341                                }
342                        }
343                        else if (file.isFile()) {
344                                if (!(repoFile instanceof NormalFile))
345                                        throw new IllegalArgumentException("repoFile is not an instance of NormalFile!");
346
347                                final NormalFile normalFile = (NormalFile) repoFile;
348                                sha(normalFile, file, new SubProgressMonitor(monitor, 100));
349                        }
350                        repoFile.setLastSyncFromRepositoryId(null);
351                        repoFile.setLastModified(new Date(file.getLastModifiedNoFollow()));
352                } finally {
353                        monitor.done();
354                }
355        }
356
357        public void deleteRepoFile(final RepoFile repoFile) {
358                deleteRepoFile(repoFile, true);
359        }
360
361        public void deleteRepoFile(final RepoFile repoFile, final boolean createDeleteModifications) {
362                final RepoFile parentRepoFile = requireNonNull(repoFile, "repoFile").getParent();
363                if (parentRepoFile == null)
364                        throw new IllegalStateException("Deleting the root is not possible!");
365
366                final PersistenceManager pm = ((co.codewizards.cloudstore.local.LocalRepoTransactionImpl)transaction).getPersistenceManager();
367
368                // We make sure, nothing interferes with our deletions (see comment below).
369                pm.flush();
370
371                if (createDeleteModifications)
372                        createDeleteModifications(repoFile);
373
374                deleteRepoFileWithAllChildrenRecursively(repoFile);
375
376                // DN batches UPDATE and DELETE statements. This sometimes causes foreign key violations and other errors in
377                // certain situations. Additionally, the deleted objects still linger in the 1st-level-cache and re-using them
378                // causes "javax.jdo.JDOUserException: Cannot read fields from a deleted object". This happens when switching
379                // from a directory to a file (or vice versa).
380                // We therefore must flush to be on the safe side. And to be extra-safe, we flush before and after deletion.
381                pm.flush();
382        }
383
384        private int getMaxCopyModificationCount(final NormalFile newNormalFile) {
385                final long fileLength = newNormalFile.getLength();
386                if (fileLength < 10 * 1024) // 10 KiB
387                        return 0;
388
389                if (fileLength < 100 * 1024) // 100 KiB
390                        return 1;
391
392                if (fileLength < 1024 * 1024) // 1 MiB
393                        return 2;
394
395                if (fileLength < 10 * 1024 * 1024) // 10 MiB
396                        return 3;
397
398                if (fileLength < 100 * 1024 * 1024) // 100 MiB
399                        return 5;
400
401                if (fileLength < 1024 * 1024 * 1024) // 1 GiB
402                        return 7;
403
404                if (fileLength < 10 * 1024 * 1024 * 1024) // 10 GiB
405                        return 9;
406
407                return 11;
408        }
409
410        protected void createCopyModificationsIfPossible(final NormalFile newNormalFile) {
411                // A CopyModification is not necessary for an empty file. And since this method is called
412                // during RepoTransport.beginPutFile(...), we easily filter out this unwanted case already.
413                // Changed to dynamic limit of CopyModifications depending on file size.
414                // The bigger the file, the more it's worth the overhead.
415                final int maxCopyModificationCount = getMaxCopyModificationCount(newNormalFile);
416                if (maxCopyModificationCount < 1)
417                        return;
418
419                final Set<String> fromPaths = new HashSet<String>();
420
421                final Set<String> paths = sha1AndLength2Paths.get(getSha1AndLength(newNormalFile.getSha1(), newNormalFile.getLength()));
422                if (paths != null) {
423                        final List<String> pathList = new ArrayList<>(paths);
424                        Collections.shuffle(pathList);
425
426                        for (final String path : pathList) {
427                                createCopyModifications(path, newNormalFile, fromPaths);
428                                if (fromPaths.size() >= maxCopyModificationCount)
429                                        return;
430                        }
431                }
432
433                final List<NormalFile> normalFiles = new ArrayList<>(normalFileDao.getNormalFilesForSha1(newNormalFile.getSha1(), newNormalFile.getLength()));
434                Collections.shuffle(normalFiles);
435                for (final NormalFile normalFile : normalFiles) {
436//                      if (normalFile.isInProgress()) // Additional check. Do we really want this? I don't think so!
437//                              continue;
438
439                        if (newNormalFile.equals(normalFile)) // should never happen, because newNormalFile is not yet persisted, but we write robust code that doesn't break easily after refactoring.
440                                continue;
441
442                        createCopyModifications(normalFile, newNormalFile, fromPaths);
443                        if (fromPaths.size() >= maxCopyModificationCount)
444                                return;
445                }
446
447                final List<DeleteModification> deleteModifications = new ArrayList<>(deleteModificationDao.getDeleteModificationsForSha1(newNormalFile.getSha1(), newNormalFile.getLength()));
448                Collections.shuffle(deleteModifications);
449                for (final DeleteModification deleteModification : deleteModifications) {
450                        createCopyModifications(deleteModification, newNormalFile, fromPaths);
451                        if (fromPaths.size() >= maxCopyModificationCount)
452                                return;
453                }
454        }
455
456        private void createCopyModifications(final DeleteModification deleteModification, final NormalFile toNormalFile, final Set<String> fromPaths) {
457                requireNonNull(deleteModification, "deleteModification");
458                requireNonNull(toNormalFile, "toNormalFile");
459                requireNonNull(fromPaths, "fromPaths");
460
461                if (deleteModification.getLength() != toNormalFile.getLength())
462                        throw new IllegalArgumentException("fromNormalFile.length != toNormalFile.length");
463
464                if (!deleteModification.getSha1().equals(toNormalFile.getSha1()))
465                        throw new IllegalArgumentException("fromNormalFile.sha1 != toNormalFile.sha1");
466
467                createCopyModifications(deleteModification.getPath(), toNormalFile, fromPaths);
468        }
469
470        private void createCopyModifications(final String fromPath, final NormalFile toNormalFile, final Set<String> fromPaths) {
471                requireNonNull(fromPath, "fromPath");
472                requireNonNull(toNormalFile, "toNormalFile");
473                requireNonNull(fromPaths, "fromPaths");
474
475                if (!fromPaths.add(fromPath)) // already done before => prevent duplicates.
476                        return;
477
478                for (final RemoteRepository remoteRepository : getRemoteRepositories()) {
479                        final CopyModification modification = new CopyModification();
480                        modification.setRemoteRepository(remoteRepository);
481                        modification.setFromPath(fromPath);
482                        modification.setToPath(toNormalFile.getPath());
483                        modification.setLength(toNormalFile.getLength());
484                        modification.setSha1(toNormalFile.getSha1());
485                        modificationDao.makePersistent(modification);
486                }
487        }
488
489        private void createCopyModifications(final NormalFile fromNormalFile, final NormalFile toNormalFile, final Set<String> fromPaths) {
490                requireNonNull(fromNormalFile, "fromNormalFile");
491                requireNonNull(toNormalFile, "toNormalFile");
492                requireNonNull(fromPaths, "fromPaths");
493
494                if (fromNormalFile.getLength() != toNormalFile.getLength())
495                        throw new IllegalArgumentException("fromNormalFile.length != toNormalFile.length");
496
497                if (!fromNormalFile.getSha1().equals(toNormalFile.getSha1()))
498                        throw new IllegalArgumentException("fromNormalFile.sha1 != toNormalFile.sha1");
499
500                createCopyModifications(fromNormalFile.getPath(), toNormalFile, fromPaths);
501        }
502
503        protected void createDeleteModifications(final RepoFile repoFile) {
504                requireNonNull(repoFile, "repoFile");
505
506                for (final RemoteRepository remoteRepository : getRemoteRepositories())
507                        createDeleteModification(repoFile, remoteRepository);
508        }
509
510        protected DeleteModification createDeleteModification(final RepoFile repoFile, final RemoteRepository remoteRepository) {
511                requireNonNull(repoFile, "repoFile");
512                requireNonNull(remoteRepository, "remoteRepository");
513                final DeleteModification modification = createObject(DeleteModification.class);
514                populateDeleteModification(modification, repoFile, remoteRepository);
515                return modificationDao.makePersistent(modification);
516        }
517
518        protected void populateDeleteModification(final DeleteModification modification, final RepoFile repoFile, final RemoteRepository remoteRepository) {
519                requireNonNull(modification, "modification");
520                requireNonNull(repoFile, "repoFile");
521                requireNonNull(remoteRepository, "remoteRepository");
522
523                final NormalFile normalFile = (repoFile instanceof NormalFile) ? (NormalFile) repoFile : null;
524
525                modification.setRemoteRepository(remoteRepository);
526                modification.setPath(repoFile.getPath());
527                modification.setLength(normalFile == null ? -1 : normalFile.getLength());
528                modification.setSha1(normalFile == null ? null : normalFile.getSha1());
529        }
530
531        private Collection<RemoteRepository> getRemoteRepositories() {
532                if (remoteRepositories == null)
533                        remoteRepositories = Collections.unmodifiableCollection(remoteRepositoryDao.getObjects());
534
535                return remoteRepositories;
536        }
537
538        protected void deleteRepoFileWithAllChildrenRecursively(final RepoFile repoFile) {
539                requireNonNull(repoFile, "repoFile");
540                for (final RepoFile childRepoFile : repoFileDao.getChildRepoFiles(repoFile)) {
541                        deleteRepoFileWithAllChildrenRecursively(childRepoFile);
542                }
543                putIntoSha1AndLength2PathsIfNormalFile(repoFile);
544                repoFileDao.deletePersistent(repoFile);
545        }
546
547        protected void putIntoSha1AndLength2PathsIfNormalFile(final RepoFile repoFile) {
548                if (repoFile instanceof NormalFile) {
549                        final NormalFile normalFile = (NormalFile) repoFile;
550                        final String sha1AndLength = getSha1AndLength(normalFile.getSha1(), normalFile.getLength());
551                        Set<String> paths = sha1AndLength2Paths.get(sha1AndLength);
552                        if (paths == null) {
553                                paths = new HashSet<>(1);
554                                sha1AndLength2Paths.put(sha1AndLength, paths);
555                        }
556                        paths.add(normalFile.getPath());
557                }
558        }
559
560        private String getSha1AndLength(final String sha1, final long length) {
561                return sha1 + ':' + length;
562        }
563
564        protected void sha(final NormalFile normalFile, final File file, final ProgressMonitor monitor) {
565                monitor.beginTask("Local sync...", (int)Math.min(file.length(), Integer.MAX_VALUE));
566                try {
567                        normalFile.getFileChunks().clear();
568                        transaction.flush();
569
570                        final MessageDigest mdAll = MessageDigest.getInstance(HashUtil.HASH_ALGORITHM_SHA);
571                        final MessageDigest mdChunk = MessageDigest.getInstance(HashUtil.HASH_ALGORITHM_SHA);
572
573                        final int bufLength = 32 * 1024;
574
575                        long offset = 0;
576
577                        try (final InputStream in = castStream(file.createInputStream())) {
578                                FileChunk fileChunk = null;
579
580                                final byte[] buf = new byte[bufLength];
581                                while (true) {
582                                        if (fileChunk == null) {
583                                                fileChunk = createObject(FileChunk.class);
584                                                fileChunk.setNormalFile(normalFile);
585                                                fileChunk.setOffset(offset);
586                                                fileChunk.setLength(0);
587                                                mdChunk.reset();
588                                        }
589
590                                        final int bytesRead = in.read(buf, 0, buf.length);
591
592                                        if (bytesRead > 0) {
593                                                mdAll.update(buf, 0, bytesRead);
594                                                mdChunk.update(buf, 0, bytesRead);
595                                                offset += bytesRead;
596                                                fileChunk.setLength(fileChunk.getLength() + bytesRead);
597                                        }
598
599                                        if (bytesRead < 0 || fileChunk.getLength() >= FileChunkDto.MAX_LENGTH) {
600                                                fileChunk.setSha1(HashUtil.encodeHexStr(mdChunk.digest()));
601                                                onFinalizeFileChunk(fileChunk);
602                                                fileChunk.makeReadOnly();
603                                                normalFile.getFileChunks().add(fileChunk);
604                                                fileChunk = null;
605
606                                                if (bytesRead < 0) {
607                                                        break;
608                                                }
609                                        }
610
611                                        if (bytesRead > 0)
612                                                monitor.worked(bytesRead);
613                                }
614                        }
615                        normalFile.setSha1(HashUtil.encodeHexStr(mdAll.digest()));
616                        normalFile.setLength(offset);
617
618                        final long fileLength = file.length(); // Important to check it now at the end.
619                        if (fileLength != offset) {
620                                logger.warn("sha: file.length() != bytesReadTotal :: File seems to be written concurrently! file='{}' file.length={} bytesReadTotal={}",
621                                                file, fileLength, offset);
622                        }
623                } catch (final NoSuchAlgorithmException e) {
624                        throw new RuntimeException(e);
625                } catch (final IOException e) {
626                        throw new RuntimeException(e);
627                } finally {
628                        monitor.done();
629                }
630        }
631
632        protected void onFinalizeFileChunk(FileChunk fileChunk) {
633                // can be extended by sub-classes to handle FileChunk-subclasses specifically.
634        }
635
636        public boolean isIgnoreRulesEnabled() {
637                return ignoreRulesEnabled;
638        }
639        public void setIgnoreRulesEnabled(boolean ignoreRulesEnabled) {
640                this.ignoreRulesEnabled = ignoreRulesEnabled;
641        }
642        public LocalRepoSync ignoreRulesEnabled(boolean ignoreRulesEnabled) {
643                setIgnoreRulesEnabled(ignoreRulesEnabled);
644                return this;
645        }
646}