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}