/*
* 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;
}
}
}