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