001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.database;
019
020import java.io.File;
021import java.io.IOException;
022import java.nio.file.Files;
023import java.nio.file.InvalidPathException;
024import java.nio.file.LinkOption;
025import java.nio.file.Path;
026import java.nio.file.Paths;
027import java.nio.file.attribute.BasicFileAttributes;
028import java.nio.file.attribute.DosFileAttributes;
029import java.nio.file.attribute.PosixFileAttributes;
030import java.nio.file.attribute.PosixFilePermissions;
031import java.security.NoSuchAlgorithmException;
032import java.util.Date;
033import java.util.HashSet;
034import java.util.Set;
035import java.util.logging.Level;
036import java.util.logging.Logger;
037
038import org.syncany.database.FileContent.FileChecksum;
039import org.syncany.database.FileVersion.FileStatus;
040import org.syncany.database.FileVersion.FileType;
041import org.syncany.util.EnvironmentUtil;
042import org.syncany.util.FileUtil;
043
044/**
045 * The file version comparator is a helper class to compare {@link FileVersion}s with each
046 * other, or compare {@link FileVersion}s to local {@link File}s.
047 *
048 * <p>It captures the {@link FileProperties} of two files or file versions and compares them
049 * using the various <code>compare*</code>-methods. A comparison returns a set of {@link FileChange}s,
050 * each of which identifies a certain attribute change (e.g. checksum changed, name changed).
051 * A file can be considered equal if the returned set of {@link FileChange}s is empty.
052 *
053 * <p>The file version comparator distinguishes between <i>cancelling</i> tests and regular tests.
054 * Cancelling tests are implemented in {@link #performCancellingTests(FileVersionComparison) performCancellingTests()}.
055 * They represent significant changes in a file, for which further comparison would not make
056 * sense (e.g. new vs. deleted files or files vs. folders). If a cancelling test is not successful,
057 * other tests are not performed.
058 *
059 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
060 */
061public class FileVersionComparator {
062        private static final Logger logger = Logger.getLogger(FileVersionComparator.class.getSimpleName());
063        private File rootFolder;
064        private String checksumAlgorithm;
065
066        /**
067         * Creates a new file version comparator helper class.
068         *
069         * <p>The <code>rootFolder</code> is needed to allow a comparison of the relative file path.
070         * The <code>checksumAlgorithm</code> is used for calculate and compare file checksums. Both
071         * are used if a local {@link File} is compared to a {@link FileVersion}.
072         *
073         * @param rootFolder Base folder to determine a relative path to
074         * @param checksumAlgorithm Digest algorithm for checksum calculation, e.g. "SHA1" or "MD5"
075         */
076        public FileVersionComparator(File rootFolder, String checksumAlgorithm) {
077                this.rootFolder = rootFolder;
078                this.checksumAlgorithm = checksumAlgorithm;
079        }
080
081        /**
082         * Compares two {@link FileVersion}s to each other and returns a {@link FileVersionComparison} object.
083         *
084         * @param expectedFileVersion The expected file version (that is compared to the actual file version)
085         * @param actualFileVersion The actual file version (that is compared to the expected file version)
086         * @return Returns a file version comparison object, indicating if there are differences between the file versions
087         */
088        public FileVersionComparison compare(FileVersion expectedFileVersion, FileVersion actualFileVersion) {
089                FileProperties expectedFileProperties = captureFileProperties(expectedFileVersion);
090                FileProperties actualFileProperties = captureFileProperties(actualFileVersion);
091
092                return compare(expectedFileProperties, actualFileProperties, true);
093        }
094
095        /**
096         * Compares a {@link FileVersion} with a local {@link File} and returns a {@link FileVersionComparison} object.
097         *
098         * <p>If the actual file does not differ in size, it is necessary to calculate and compare the checksum of the
099         * local file to the file version to reliably determine if it has changed. Unless comparing the size and last
100         * modified date is enough, the <code>actualFileForceChecksum</code> parameter must be switched to <code>true</code>.
101         *
102         * @param expectedFileVersion The expected file version (that is compared to the actual file)
103         * @param actualFile The actual file (that is compared to the expected file version)
104         * @param actualFileForceChecksum Force a checksum comparison if necessary (if size does not differ)
105         * @return Returns a file version comparison object, indicating if there are differences between the file versions
106         */
107        public FileVersionComparison compare(FileVersion expectedFileVersion, File actualFile, boolean actualFileForceChecksum) {
108                return compare(expectedFileVersion, actualFile, null, actualFileForceChecksum);
109        }
110
111        /**
112         * Compares a {@link FileVersion} with a local {@link File} and returns a {@link FileVersionComparison} object.
113         *
114         * <p>If the actual file does not differ in size, it is necessary to calculate and compare the checksum of the
115         * local file to the file version to reliably determine if it has changed. Unless comparing the size and last
116         * modified date is enough, the <code>actualFileForceChecksum</code> parameter must be switched to <code>true</code>.
117         *
118         * <p>If the <code>actualFileKnownChecksum</code> parameter is set and a checksum comparison is necessary, this
119         * parameter is used to compare checksums. If not and force checksum is enabled, the checksum is calculated
120         * and compared.
121         *
122         * @param expectedLocalFileVersion The expected file version (that is compared to the actual file)
123         * @param actualLocalFile The actual file (that is compared to the expected file version)
124         * @param actualFileKnownChecksum If the checksum of the local file is known, it can be set
125         * @param actualFileForceChecksum Force a checksum comparison if necessary (if size does not differ)
126         * @return Returns a file version comparison object, indicating if there are differences between the file versions
127         */
128        public FileVersionComparison compare(FileVersion expectedLocalFileVersion, File actualLocalFile, FileChecksum actualFileKnownChecksum,
129                        boolean actualFileForceChecksum) {
130
131                FileProperties expectedLocalFileVersionProperties = captureFileProperties(expectedLocalFileVersion);
132                FileProperties actualFileProperties = captureFileProperties(actualLocalFile, actualFileKnownChecksum, actualFileForceChecksum);
133
134                return compare(expectedLocalFileVersionProperties, actualFileProperties, actualFileForceChecksum);
135        }
136
137        public FileVersionComparison compare(FileProperties expectedFileProperties, FileProperties actualFileProperties, boolean compareChecksums) {
138                FileVersionComparison fileComparison = new FileVersionComparison();
139
140                fileComparison.fileChanges = new HashSet<FileChange>();
141                fileComparison.expectedFileProperties = expectedFileProperties;
142                fileComparison.actualFileProperties = actualFileProperties;
143
144                boolean cancelFurtherTests = performCancellingTests(fileComparison);
145
146                if (cancelFurtherTests) {
147                        return fileComparison;
148                }
149
150                switch (actualFileProperties.getType()) {
151                case FILE:
152                        compareFile(fileComparison, compareChecksums);
153                        break;
154
155                case FOLDER:
156                        compareFolder(fileComparison);
157                        break;
158
159                case SYMLINK:
160                        compareSymlink(fileComparison);
161                        break;
162
163                default:
164                        throw new RuntimeException("This should not happen. Unknown file type: " + actualFileProperties.getType());
165                }
166
167                return fileComparison;
168        }
169
170        private void compareSymlink(FileVersionComparison fileComparison) {
171                // comparePath(fileComparison);
172                compareSymlinkTarget(fileComparison);
173        }
174
175        private void compareFolder(FileVersionComparison fileComparison) {
176                // comparePath(fileComparison);
177                compareAttributes(fileComparison);
178        }
179
180        private void compareFile(FileVersionComparison fileComparison, boolean compareChecksums) {
181                comparePath(fileComparison);
182                compareModifiedDate(fileComparison);
183                compareSize(fileComparison);
184                compareAttributes(fileComparison);
185
186                // Check if checksum comparison necessary
187                if (fileComparison.getFileChanges().contains(FileChange.CHANGED_SIZE)) {
188                        fileComparison.fileChanges.add(FileChange.CHANGED_CHECKSUM);
189                }
190                else if (compareChecksums) {
191                        compareChecksum(fileComparison);
192                }
193        }
194
195        private void compareChecksum(FileVersionComparison fileComparison) {
196                boolean isChecksumEqual = FileChecksum.fileChecksumEquals(fileComparison.expectedFileProperties.getChecksum(),
197                                fileComparison.actualFileProperties.getChecksum());
198
199                if (!isChecksumEqual) {
200                        fileComparison.fileChanges.add(FileChange.CHANGED_CHECKSUM);
201
202                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
203                                        + ": Local file DIFFERS from file version, expected CHECKSUM = {0}, but actual CHECKSUM = {1}, for file {2}",
204                                        new Object[] { fileComparison.expectedFileProperties.checksum, fileComparison.actualFileProperties.checksum,
205                                                        fileComparison.actualFileProperties.getRelativePath() });
206                }
207        }
208
209        private void compareSymlinkTarget(FileVersionComparison fileComparison) {
210                boolean linkTargetsIdentical = fileComparison.expectedFileProperties.getLinkTarget() != null
211                                && fileComparison.expectedFileProperties.getLinkTarget().equals(fileComparison.actualFileProperties.getLinkTarget());
212
213                if (!linkTargetsIdentical) {
214                        fileComparison.fileChanges.add(FileChange.CHANGED_LINK_TARGET);
215
216                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
217                                        + ": Local file DIFFERS from file version, expected LINK TARGET = {0}, but actual LINK TARGET = {1}, for file {2}", new Object[] {
218                                        fileComparison.actualFileProperties.getLinkTarget(), fileComparison.expectedFileProperties.getLinkTarget(),
219                                        fileComparison.actualFileProperties.getRelativePath() });
220                }
221        }
222
223        private void compareAttributes(FileVersionComparison fileComparison) {
224                if (EnvironmentUtil.isWindows()) {
225                        compareDosAttributes(fileComparison);
226                }
227                else if (EnvironmentUtil.isUnixLikeOperatingSystem()) {
228                        comparePosixPermissions(fileComparison);
229                }
230        }
231
232        private void comparePosixPermissions(FileVersionComparison fileComparison) {
233                boolean posixPermsDiffer = false;
234
235                boolean actualIsNull = fileComparison.actualFileProperties == null || fileComparison.actualFileProperties.getPosixPermissions() == null;
236                boolean expectedIsNull = fileComparison.expectedFileProperties == null || fileComparison.expectedFileProperties.getPosixPermissions() == null;
237
238                if (!actualIsNull && !expectedIsNull) {
239                        if (!fileComparison.actualFileProperties.getPosixPermissions().equals(fileComparison.expectedFileProperties.getPosixPermissions())) {
240                                posixPermsDiffer = true;
241                        }
242                }
243                else if ((actualIsNull && !expectedIsNull) || (!actualIsNull && expectedIsNull)) {
244                        posixPermsDiffer = true;
245                }
246
247                if (posixPermsDiffer) {
248                        fileComparison.fileChanges.add(FileChange.CHANGED_ATTRIBUTES);
249
250                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
251                                        + ": Local file DIFFERS from file version, expected POSIX ATTRS = {0}, but actual POSIX ATTRS = {1}, for file {2}", new Object[] {
252                                        fileComparison.expectedFileProperties.getPosixPermissions(), fileComparison.actualFileProperties.getPosixPermissions(),
253                                        fileComparison.actualFileProperties.getRelativePath() });
254                }
255        }
256
257        private void compareDosAttributes(FileVersionComparison fileComparison) {
258                boolean dosAttrsDiffer = false;
259
260                boolean actualIsNull = fileComparison.actualFileProperties == null || fileComparison.actualFileProperties.getDosAttributes() == null;
261                boolean expectedIsNull = fileComparison.expectedFileProperties == null || fileComparison.expectedFileProperties.getDosAttributes() == null;
262
263                if (!actualIsNull && !expectedIsNull) {
264                        if (!fileComparison.actualFileProperties.getDosAttributes().equals(fileComparison.expectedFileProperties.getDosAttributes())) {
265                                dosAttrsDiffer = true;
266                        }
267                }
268                else if ((actualIsNull && !expectedIsNull) || (!actualIsNull && expectedIsNull)) {
269                        dosAttrsDiffer = true;
270                }
271
272                if (dosAttrsDiffer) {
273                        fileComparison.fileChanges.add(FileChange.CHANGED_ATTRIBUTES);
274
275                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
276                                        + ": Local file DIFFERS from file version, expected DOS ATTRS = {0}, but actual DOS ATTRS = {1}, for file {2}", new Object[] {
277                                        fileComparison.expectedFileProperties.getDosAttributes(), fileComparison.actualFileProperties.getDosAttributes(),
278                                        fileComparison.actualFileProperties.getRelativePath() });
279                }
280        }
281
282        private void compareSize(FileVersionComparison fileComparison) {
283                if (fileComparison.expectedFileProperties.getSize() != fileComparison.actualFileProperties.getSize()) {
284                        fileComparison.fileChanges.add(FileChange.CHANGED_SIZE);
285
286                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
287                                        + ": Local file DIFFERS from file version, expected SIZE = {0}, but actual SIZE = {1}, for file {2}", new Object[] {
288                                        fileComparison.expectedFileProperties.getSize(), fileComparison.actualFileProperties.getSize(),
289                                        fileComparison.actualFileProperties.getRelativePath() });
290                }
291        }
292
293        private void compareModifiedDate(FileVersionComparison fileComparison) {
294                long timeDifferenceMillis = Math.abs(fileComparison.expectedFileProperties.getLastModified()
295                                - fileComparison.actualFileProperties.getLastModified());
296
297                // Fuzziness on last modified dates is necessary, see issue #166
298
299                if (timeDifferenceMillis > 1000) {
300                        fileComparison.fileChanges.add(FileChange.CHANGED_LAST_MOD_DATE);
301
302                        logger.log(
303                                        Level.INFO,
304                                        "     - "
305                                                        + fileComparison.fileChanges
306                                                        + ": Local file DIFFERS from file version, expected MOD. DATE = {0} ({1}), but actual MOD. DATE = {2} ({3}), for file {4}",
307                                                        new Object[] {
308                                                        new Date(fileComparison.expectedFileProperties.getLastModified()),
309                                                        fileComparison.expectedFileProperties.getLastModified(),
310                                                        new Date(fileComparison.actualFileProperties.getLastModified()), fileComparison.actualFileProperties.getLastModified(),
311                                                        fileComparison.actualFileProperties.getRelativePath() });
312                }
313        }
314
315        private void comparePath(FileVersionComparison fileComparison) {
316                if (!fileComparison.expectedFileProperties.getRelativePath().equals(fileComparison.actualFileProperties.getRelativePath())) {
317                        fileComparison.fileChanges.add(FileChange.CHANGED_PATH);
318
319                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
320                                        + ": Local file DIFFERS from file version, expected PATH = {0}, but actual PATH = {1}, for file {2}", new Object[] {
321                                        fileComparison.expectedFileProperties.getRelativePath(), fileComparison.actualFileProperties.getRelativePath(),
322                                        fileComparison.actualFileProperties.getRelativePath() });
323                }
324        }
325
326        private boolean performCancellingTests(FileVersionComparison fileComparison) {
327                // Check null
328                if (fileComparison.actualFileProperties == null && fileComparison.expectedFileProperties == null) {
329                        throw new RuntimeException("actualFileProperties and expectedFileProperties cannot be null.");
330                }
331                else if (fileComparison.actualFileProperties != null && fileComparison.expectedFileProperties == null) {
332                        throw new RuntimeException("expectedFileProperties cannot be null.");
333                }
334                else if (fileComparison.actualFileProperties == null && fileComparison.expectedFileProperties != null) {
335                        if (!fileComparison.expectedFileProperties.exists()) {
336                                logger.log(Level.INFO, "     - " + fileComparison.fileChanges
337                                                + ": Local file does not exist, and expected file was deleted, for file {0}",
338                                                new Object[] { fileComparison.expectedFileProperties.getRelativePath() });
339
340                                return true;
341                        }
342                        else {
343                                fileComparison.fileChanges.add(FileChange.DELETED);
344
345                                logger.log(Level.INFO, "     - " + fileComparison.fileChanges
346                                                + ": Local file DIFFERS from file version, actual file is NULL, for file {0}",
347                                                new Object[] { fileComparison.expectedFileProperties.getRelativePath() });
348
349                                return true;
350                        }
351                }
352
353                // Check existence
354                if (fileComparison.expectedFileProperties.exists() != fileComparison.actualFileProperties.exists()) {
355                        // File is expected to exist, but it does NOT --> file has been deleted
356                        if (fileComparison.expectedFileProperties.exists() && !fileComparison.actualFileProperties.exists()) {
357                                fileComparison.fileChanges.add(FileChange.DELETED);
358                        }
359
360                        // File is expected to NOT exist, but it does --> file is new
361                        else {
362                                fileComparison.fileChanges.add(FileChange.NEW);
363                        }
364
365                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
366                                        + ": Local file DIFFERS from file version, expected EXISTS = {0}, but actual EXISTS = {1}, for file {2}",
367                                        new Object[] { fileComparison.expectedFileProperties.exists(), fileComparison.actualFileProperties.exists(),
368                                                        fileComparison.actualFileProperties.getRelativePath() });
369
370                        return true;
371                }
372                else if (!fileComparison.expectedFileProperties.exists() && !fileComparison.actualFileProperties.exists()) {
373                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
374                                        + ": Local file does not exist, and expected file was deleted, for file {0}",
375                                        new Object[] { fileComparison.expectedFileProperties.getRelativePath() });
376
377                        return true;
378                }
379
380                // Check file type (folder/file)
381                if (!fileComparison.expectedFileProperties.getType().equals(fileComparison.actualFileProperties.getType())) {
382                        fileComparison.fileChanges.add(FileChange.DELETED);
383
384                        logger.log(Level.INFO, "     - " + fileComparison.fileChanges
385                                        + ": Local file DIFFERS from file version, expected TYPE = {0}, but actual TYPE = {1}, for file {2}", new Object[] {
386                                        fileComparison.expectedFileProperties.getType(), fileComparison.actualFileProperties.getType(),
387                                        fileComparison.actualFileProperties.getRelativePath() });
388
389                        return true;
390                }
391
392                return false;
393        }
394
395        public FileProperties captureFileProperties(File file, FileChecksum knownChecksum, boolean forceChecksum) {
396                FileProperties fileProperties = new FileProperties();
397                fileProperties.relativePath = FileUtil.getRelativeDatabasePath(rootFolder, file);
398
399                Path filePath = null;
400
401                try {
402                        filePath = Paths.get(file.getAbsolutePath());
403                        fileProperties.exists = Files.exists(filePath, LinkOption.NOFOLLOW_LINKS);
404                }
405                catch (InvalidPathException e) {
406                        // This throws an exception if the filename is invalid,
407                        // e.g. colon in filename on windows "file:name"
408
409                        logger.log(Level.FINE, "InvalidPath", e);
410                        logger.log(Level.WARNING, "- Path '{0}' is invalid on this file system. It cannot exist. ", file.getAbsolutePath());
411
412                        fileProperties.exists = false;
413                        return fileProperties;
414                }
415
416                if (!fileProperties.exists) {
417                        return fileProperties;
418                }
419
420                try {
421                        // Read operating system dependent file attributes
422                        BasicFileAttributes fileAttributes = null;
423
424                        if (EnvironmentUtil.isWindows()) {
425                                DosFileAttributes dosAttrs = Files.readAttributes(filePath, DosFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
426                                fileProperties.dosAttributes = FileUtil.dosAttrsToString(dosAttrs);
427
428                                fileAttributes = dosAttrs;
429                        }
430                        else if (EnvironmentUtil.isUnixLikeOperatingSystem()) {
431                                PosixFileAttributes posixAttrs = Files.readAttributes(filePath, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
432                                fileProperties.posixPermissions = PosixFilePermissions.toString(posixAttrs.permissions());
433
434                                fileAttributes = posixAttrs;
435                        }
436                        else {
437                                fileAttributes = Files.readAttributes(filePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
438                        }
439
440                        fileProperties.lastModified = fileAttributes.lastModifiedTime().toMillis();
441                        fileProperties.size = fileAttributes.size();
442
443                        // Type
444                        if (fileAttributes.isSymbolicLink()) {
445                                fileProperties.type = FileType.SYMLINK;
446                                fileProperties.linkTarget = FileUtil.readSymlinkTarget(file);
447                        }
448                        else if (fileAttributes.isDirectory()) {
449                                fileProperties.type = FileType.FOLDER;
450                                fileProperties.linkTarget = null;
451                        }
452                        else {
453                                fileProperties.type = FileType.FILE;
454                                fileProperties.linkTarget = null;
455                        }
456
457                        // Checksum
458                        if (knownChecksum != null) {
459                                fileProperties.checksum = knownChecksum;
460                        }
461                        else {
462                                if (fileProperties.type == FileType.FILE && forceChecksum) {
463                                        try {
464                                                if (fileProperties.size > 0) {
465                                                        fileProperties.checksum = new FileChecksum(FileUtil.createChecksum(file, checksumAlgorithm));
466                                                }
467                                                else {
468                                                        fileProperties.checksum = null;
469                                                }
470                                        }
471                                        catch (NoSuchAlgorithmException | IOException e) {
472                                                logger.log(Level.FINE, "Failed create checksum", e);
473                                                logger.log(Level.SEVERE, "SEVERE: Unable to create checksum for file {0}", file);
474                                                fileProperties.checksum = null;
475                                        }
476                                }
477                                else {
478                                        fileProperties.checksum = null;
479                                }
480                        }
481
482                        // Must be last (!), used for vanish-test later
483                        fileProperties.exists = Files.exists(filePath, LinkOption.NOFOLLOW_LINKS);
484                        fileProperties.locked = fileProperties.exists && FileUtil.isFileLocked(file);
485
486                        return fileProperties;
487                }
488                catch (IOException e) {
489                        logger.log(Level.FINE, "Failed to read file", e);
490                        logger.log(Level.SEVERE, "SEVERE: Cannot read file {0}. Assuming file is locked.", file);
491
492                        fileProperties.exists = true;
493                        fileProperties.locked = true;
494
495                        return fileProperties;
496                }
497        }
498
499        public FileProperties captureFileProperties(FileVersion fileVersion) {
500                if (fileVersion == null) {
501                        return null;
502                }
503
504                FileProperties fileProperties = new FileProperties();
505
506                fileProperties.lastModified = fileVersion.getLastModified().getTime();
507                fileProperties.size = fileVersion.getSize();
508                fileProperties.relativePath = fileVersion.getPath();
509                fileProperties.linkTarget = fileVersion.getLinkTarget();
510                fileProperties.checksum = fileVersion.getChecksum();
511                fileProperties.type = fileVersion.getType();
512                fileProperties.posixPermissions = fileVersion.getPosixPermissions();
513                fileProperties.dosAttributes = fileVersion.getDosAttributes();
514                fileProperties.exists = fileVersion.getStatus() != FileStatus.DELETED;
515                fileProperties.locked = false;
516
517                return fileProperties;
518        }
519
520        public static class FileVersionComparison {
521                private Set<FileChange> fileChanges = new HashSet<FileChange>();
522                private FileProperties actualFileProperties;
523                private FileProperties expectedFileProperties;
524
525                public boolean areEqual() {
526                        return fileChanges.size() == 0;
527                }
528
529                public Set<FileChange> getFileChanges() {
530                        return fileChanges;
531                }
532
533                public FileProperties getActualFileProperties() {
534                        return actualFileProperties;
535                }
536
537                public FileProperties getExpectedFileProperties() {
538                        return expectedFileProperties;
539                }
540        }
541
542        public static enum FileChange {
543                NEW, CHANGED_CHECKSUM, CHANGED_ATTRIBUTES, CHANGED_LAST_MOD_DATE, CHANGED_LINK_TARGET, CHANGED_SIZE, CHANGED_PATH, DELETED,
544        }
545
546        public static class FileProperties {
547                private long lastModified = -1;
548                private FileType type = null;
549                private long size = -1;
550                private String relativePath;
551                private String linkTarget;
552                private FileChecksum checksum = null;
553                private boolean locked = true;
554                private boolean exists = false;
555
556                private String posixPermissions = null;
557                private String dosAttributes = null;
558
559                public long getLastModified() {
560                        return lastModified;
561                }
562
563                public FileType getType() {
564                        return type;
565                }
566
567                public long getSize() {
568                        return size;
569                }
570
571                public String getRelativePath() {
572                        return relativePath;
573                }
574
575                public String getLinkTarget() {
576                        return linkTarget;
577                }
578
579                public FileChecksum getChecksum() {
580                        return checksum;
581                }
582
583                public boolean isLocked() {
584                        return locked;
585                }
586
587                public boolean exists() {
588                        return exists;
589                }
590
591                public String getPosixPermissions() {
592                        return posixPermissions;
593                }
594
595                public String getDosAttributes() {
596                        return dosAttributes;
597                }
598        }
599}