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}