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.operations.status; 019 020import java.io.File; 021import java.io.FileNotFoundException; 022import java.io.IOException; 023import java.nio.file.FileVisitResult; 024import java.nio.file.FileVisitor; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.nio.file.Paths; 028import java.nio.file.attribute.BasicFileAttributes; 029import java.util.Map; 030import java.util.logging.Level; 031import java.util.logging.Logger; 032 033import org.syncany.config.Config; 034import org.syncany.config.LocalEventBus; 035import org.syncany.database.FileVersion; 036import org.syncany.database.FileVersion.FileStatus; 037import org.syncany.database.FileVersionComparator; 038import org.syncany.database.FileVersionComparator.FileVersionComparison; 039import org.syncany.database.SqlDatabase; 040import org.syncany.operations.ChangeSet; 041import org.syncany.operations.Operation; 042import org.syncany.operations.daemon.messages.StatusEndSyncExternalEvent; 043import org.syncany.operations.daemon.messages.StatusStartSyncExternalEvent; 044import org.syncany.util.FileUtil; 045 046/** 047 * The status operation analyzes the local file tree and compares it to the current local 048 * database. It uses the {@link FileVersionComparator} to determine differences and returns 049 * new/changed/deleted files in form of a {@link ChangeSet}. 050 * 051 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 052 */ 053public class StatusOperation extends Operation { 054 private static final Logger logger = Logger.getLogger(StatusOperation.class.getSimpleName()); 055 056 private FileVersionComparator fileVersionComparator; 057 private SqlDatabase localDatabase; 058 private StatusOperationOptions options; 059 060 private LocalEventBus eventBus; 061 062 public StatusOperation(Config config) { 063 this(config, new StatusOperationOptions()); 064 } 065 066 public StatusOperation(Config config, StatusOperationOptions options) { 067 super(config); 068 069 this.fileVersionComparator = new FileVersionComparator(config.getLocalDir(), config.getChunker().getChecksumAlgorithm()); 070 this.localDatabase = new SqlDatabase(config); 071 this.options = options; 072 073 this.eventBus = LocalEventBus.getInstance(); 074 } 075 076 @Override 077 public StatusOperationResult execute() throws Exception { 078 logger.log(Level.INFO, ""); 079 logger.log(Level.INFO, "Running 'Status' at client " + config.getMachineName() + " ..."); 080 logger.log(Level.INFO, "--------------------------------------------"); 081 082 if (options != null && options.isForceChecksum()) { 083 logger.log(Level.INFO, "Force checksum ENABLED."); 084 } 085 086 if (options != null && !options.isDelete()) { 087 logger.log(Level.INFO, "Delete missing files DISABLED."); 088 } 089 090 // Get local database 091 logger.log(Level.INFO, "Querying current file tree from database ..."); 092 eventBus.post(new StatusStartSyncExternalEvent(config.getLocalDir().getAbsolutePath())); 093 094 // Path to actual file version 095 final Map<String, FileVersion> filesInDatabase = localDatabase.getCurrentFileTree(); 096 097 // Find local changes 098 logger.log(Level.INFO, "Analyzing local folder " + config.getLocalDir() + " ..."); 099 ChangeSet localChanges = findLocalChanges(filesInDatabase); 100 101 if (!localChanges.hasChanges()) { 102 logger.log(Level.INFO, "- No changes to local database"); 103 } 104 105 // Return result 106 StatusOperationResult statusResult = new StatusOperationResult(); 107 statusResult.setChangeSet(localChanges); 108 109 eventBus.post(new StatusEndSyncExternalEvent(config.getLocalDir().getAbsolutePath(), localChanges.hasChanges())); 110 111 return statusResult; 112 } 113 114 private ChangeSet findLocalChanges(final Map<String, FileVersion> filesInDatabase) throws FileNotFoundException, IOException { 115 ChangeSet localChanges = findLocalChangedAndNewFiles(config.getLocalDir(), filesInDatabase); 116 117 if (options == null || options.isDelete()) { 118 findAndAppendDeletedFiles(localChanges, filesInDatabase); 119 } 120 121 return localChanges; 122 } 123 124 private ChangeSet findLocalChangedAndNewFiles(final File root, Map<String, FileVersion> filesInDatabase) 125 throws FileNotFoundException, IOException { 126 Path rootPath = Paths.get(root.getAbsolutePath()); 127 128 StatusFileVisitor fileVisitor = new StatusFileVisitor(rootPath, filesInDatabase); 129 Files.walkFileTree(rootPath, fileVisitor); 130 131 return fileVisitor.getChangeSet(); 132 } 133 134 private void findAndAppendDeletedFiles(ChangeSet localChanges, Map<String, FileVersion> filesInDatabase) { 135 for (FileVersion lastLocalVersion : filesInDatabase.values()) { 136 // Check if file exists, remove if it doesn't 137 File lastLocalVersionOnDisk = new File(config.getLocalDir() + File.separator + lastLocalVersion.getPath()); 138 139 // Ignore this file history if the last version is marked "DELETED" 140 if (lastLocalVersion.getStatus() == FileStatus.DELETED) { 141 continue; 142 } 143 144 // If file has VANISHED, mark as DELETED 145 if (!FileUtil.exists(lastLocalVersionOnDisk)) { 146 localChanges.getDeletedFiles().add(lastLocalVersion.getPath()); 147 } 148 } 149 } 150 151 private class StatusFileVisitor implements FileVisitor<Path> { 152 private Path root; 153 private ChangeSet changeSet; 154 private Map<String, FileVersion> currentFileTree; 155 156 public StatusFileVisitor(Path root, Map<String, FileVersion> currentFileTree) { 157 this.root = root; 158 this.changeSet = new ChangeSet(); 159 this.currentFileTree = currentFileTree; 160 } 161 162 public ChangeSet getChangeSet() { 163 return changeSet; 164 } 165 166 @Override 167 public FileVisitResult visitFile(Path actualLocalFile, BasicFileAttributes attrs) throws IOException { 168 String relativeFilePath = FileUtil.getRelativeDatabasePath(root.toFile(), actualLocalFile.toFile()); //root.relativize(actualLocalFile).toString(); 169 170 // Skip Syncany root folder 171 if (actualLocalFile.toFile().equals(config.getLocalDir())) { 172 return FileVisitResult.CONTINUE; 173 } 174 175 // Skip .syncany (or app related acc. to config) 176 boolean isAppRelatedDir = actualLocalFile.toFile().equals(config.getAppDir()) 177 || actualLocalFile.toFile().equals(config.getCache()) 178 || actualLocalFile.toFile().equals(config.getDatabaseDir()) 179 || actualLocalFile.toFile().equals(config.getLogDir()); 180 181 if (isAppRelatedDir) { 182 logger.log(Level.FINEST, "- Ignoring file (syncany app-related): {0}", relativeFilePath); 183 return FileVisitResult.SKIP_SUBTREE; 184 } 185 186 // Check if file is locked 187 boolean fileLocked = FileUtil.isFileLocked(actualLocalFile.toFile()); 188 189 if (fileLocked) { 190 logger.log(Level.FINEST, "- Ignoring file (locked): {0}", relativeFilePath); 191 return FileVisitResult.CONTINUE; 192 } 193 194 // Check database by file path 195 FileVersion expectedLastFileVersion = currentFileTree.get(relativeFilePath); 196 197 if (expectedLastFileVersion != null) { 198 // Compare 199 boolean forceChecksum = options != null && options.isForceChecksum(); 200 FileVersionComparison fileVersionComparison = fileVersionComparator.compare(expectedLastFileVersion, actualLocalFile.toFile(), 201 forceChecksum); 202 203 if (fileVersionComparison.areEqual()) { 204 changeSet.getUnchangedFiles().add(relativeFilePath); 205 } 206 else { 207 changeSet.getChangedFiles().add(relativeFilePath); 208 } 209 } 210 else { 211 if (!config.getIgnoredFiles().isFileIgnored(relativeFilePath, actualLocalFile.toFile().getName())) { 212 changeSet.getNewFiles().add(relativeFilePath); 213 logger.log(Level.FINEST, "- New file: " + relativeFilePath); 214 } 215 else { 216 logger.log(Level.FINEST, "- Ignoring file; " + relativeFilePath); 217 return FileVisitResult.SKIP_SUBTREE; 218 } 219 } 220 221 // Check if file is symlink directory 222 boolean isSymlinkDir = attrs.isDirectory() && attrs.isSymbolicLink(); 223 224 if (isSymlinkDir) { 225 logger.log(Level.FINEST, " + File is sym. directory. Skipping subtree."); 226 return FileVisitResult.SKIP_SUBTREE; 227 } 228 else { 229 return FileVisitResult.CONTINUE; 230 } 231 } 232 233 @Override 234 public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 235 return visitFile(dir, attrs); 236 } 237 238 @Override 239 public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { 240 return FileVisitResult.CONTINUE; 241 } 242 243 @Override 244 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 245 return FileVisitResult.CONTINUE; 246 } 247 } 248}