/* * Syncany, www.syncany.org * Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.syncany.operations.status; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.syncany.config.Config; import org.syncany.config.LocalEventBus; import org.syncany.database.FileVersion; import org.syncany.database.FileVersion.FileStatus; import org.syncany.database.FileVersionComparator; import org.syncany.database.FileVersionComparator.FileVersionComparison; import org.syncany.database.SqlDatabase; import org.syncany.operations.ChangeSet; import org.syncany.operations.Operation; import org.syncany.operations.daemon.messages.StatusEndSyncExternalEvent; import org.syncany.operations.daemon.messages.StatusStartSyncExternalEvent; import org.syncany.util.FileUtil; /** * The status operation analyzes the local file tree and compares it to the current local * database. It uses the {@link FileVersionComparator} to determine differences and returns * new/changed/deleted files in form of a {@link ChangeSet}. * * @author Philipp C. Heckel <philipp.heckel@gmail.com> */ public class StatusOperation extends Operation { private static final Logger logger = Logger.getLogger(StatusOperation.class.getSimpleName()); private FileVersionComparator fileVersionComparator; private SqlDatabase localDatabase; private StatusOperationOptions options; private LocalEventBus eventBus; public StatusOperation(Config config) { this(config, new StatusOperationOptions()); } public StatusOperation(Config config, StatusOperationOptions options) { super(config); this.fileVersionComparator = new FileVersionComparator(config.getLocalDir(), config.getChunker().getChecksumAlgorithm()); this.localDatabase = new SqlDatabase(config); this.options = options; this.eventBus = LocalEventBus.getInstance(); } @Override public StatusOperationResult execute() throws Exception { logger.log(Level.INFO, ""); logger.log(Level.INFO, "Running 'Status' at client "+config.getMachineName()+" ..."); logger.log(Level.INFO, "--------------------------------------------"); if (options != null && options.isForceChecksum()) { logger.log(Level.INFO, "Force checksum ENABLED."); } if (options != null && !options.isDelete()) { logger.log(Level.INFO, "Delete missing files DISABLED."); } // Get local database logger.log(Level.INFO, "Querying current file tree from database ..."); eventBus.post(new StatusStartSyncExternalEvent(config.getLocalDir().getAbsolutePath())); // Path to actual file version final Map<String, FileVersion> filesInDatabase = localDatabase.getCurrentFileTree(); // Find local changes logger.log(Level.INFO, "Analyzing local folder "+config.getLocalDir()+" ..."); ChangeSet localChanges = findLocalChanges(filesInDatabase); if (!localChanges.hasChanges()) { logger.log(Level.INFO, "- No changes to local database"); } // Return result StatusOperationResult statusResult = new StatusOperationResult(); statusResult.setChangeSet(localChanges); eventBus.post(new StatusEndSyncExternalEvent(config.getLocalDir().getAbsolutePath(), localChanges.hasChanges())); return statusResult; } private ChangeSet findLocalChanges(final Map<String, FileVersion> filesInDatabase) throws FileNotFoundException, IOException { ChangeSet localChanges = findLocalChangedAndNewFiles(config.getLocalDir(), filesInDatabase); if (options == null || options.isDelete()) { findAndAppendDeletedFiles(localChanges, filesInDatabase); } return localChanges; } private ChangeSet findLocalChangedAndNewFiles(final File root, Map<String, FileVersion> filesInDatabase) throws FileNotFoundException, IOException { Path rootPath = Paths.get(root.getAbsolutePath()); StatusFileVisitor fileVisitor = new StatusFileVisitor(rootPath, filesInDatabase); Files.walkFileTree(rootPath, fileVisitor); return fileVisitor.getChangeSet(); } private void findAndAppendDeletedFiles(ChangeSet localChanges, Map<String,FileVersion> filesInDatabase) { for (FileVersion lastLocalVersion : filesInDatabase.values()) { // Check if file exists, remove if it doesn't File lastLocalVersionOnDisk = new File(config.getLocalDir()+File.separator+lastLocalVersion.getPath()); // Ignore this file history if the last version is marked "DELETED" if (lastLocalVersion.getStatus() == FileStatus.DELETED) { continue; } // If file has VANISHED, mark as DELETED if (!FileUtil.exists(lastLocalVersionOnDisk)) { localChanges.getDeletedFiles().add(lastLocalVersion.getPath()); } } } private class StatusFileVisitor implements FileVisitor<Path> { private Path root; private ChangeSet changeSet; private Map<String, FileVersion> currentFileTree; public StatusFileVisitor(Path root, Map<String, FileVersion> currentFileTree) { this.root = root; this.changeSet = new ChangeSet(); this.currentFileTree = currentFileTree; } public ChangeSet getChangeSet() { return changeSet; } @Override public FileVisitResult visitFile(Path actualLocalFile, BasicFileAttributes attrs) throws IOException { String relativeFilePath = FileUtil.getRelativeDatabasePath(root.toFile(), actualLocalFile.toFile()); //root.relativize(actualLocalFile).toString(); // Skip Syncany root folder if (actualLocalFile.toFile().equals(config.getLocalDir())) { return FileVisitResult.CONTINUE; } // Skip .syncany (or app related acc. to config) boolean isAppRelatedDir = actualLocalFile.toFile().equals(config.getAppDir()) || actualLocalFile.toFile().equals(config.getCache()) || actualLocalFile.toFile().equals(config.getDatabaseDir()) || actualLocalFile.toFile().equals(config.getLogDir()); if (isAppRelatedDir) { logger.log(Level.FINEST, "- Ignoring file (syncany app-related): {0}", relativeFilePath); return FileVisitResult.SKIP_SUBTREE; } // Check if file is locked boolean fileLocked = FileUtil.isFileLocked(actualLocalFile.toFile()); if (fileLocked) { logger.log(Level.FINEST, "- Ignoring file (locked): {0}", relativeFilePath); return FileVisitResult.CONTINUE; } // Check database by file path FileVersion expectedLastFileVersion = currentFileTree.get(relativeFilePath); if (expectedLastFileVersion != null) { // Compare boolean forceChecksum = options != null && options.isForceChecksum(); FileVersionComparison fileVersionComparison = fileVersionComparator.compare(expectedLastFileVersion, actualLocalFile.toFile(), forceChecksum); if (fileVersionComparison.areEqual()) { changeSet.getUnchangedFiles().add(relativeFilePath); } else { changeSet.getChangedFiles().add(relativeFilePath); } } else { if (!config.getIgnoredFiles().isFileIgnored(relativeFilePath)) { changeSet.getNewFiles().add(relativeFilePath); logger.log(Level.FINEST, "- New file: "+relativeFilePath); } else { logger.log(Level.FINEST, "- Ignoring file; " + relativeFilePath); return FileVisitResult.SKIP_SUBTREE; } } // Check if file is symlink directory boolean isSymlinkDir = attrs.isDirectory() && attrs.isSymbolicLink(); if (isSymlinkDir) { logger.log(Level.FINEST, " + File is sym. directory. Skipping subtree."); return FileVisitResult.SKIP_SUBTREE; } else { return FileVisitResult.CONTINUE; } } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { return visitFile(dir, attrs); } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } } }