/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2016 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.init;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.logging.Level;
import org.syncany.config.Config;
import org.syncany.config.DaemonConfigHelper;
import org.syncany.config.to.ConfigTO;
import org.syncany.config.to.MasterTO;
import org.syncany.config.to.RepoTO;
import org.syncany.crypto.CipherUtil;
import org.syncany.crypto.SaltedSecretKey;
import org.syncany.operations.init.InitOperationResult.InitResultCode;
import org.syncany.plugins.UserInteractionListener;
import org.syncany.plugins.transfer.StorageException;
import org.syncany.plugins.transfer.StorageTestResult;
import org.syncany.plugins.transfer.TransferManager;
import org.syncany.plugins.transfer.files.MasterRemoteFile;
import org.syncany.plugins.transfer.files.SyncanyRemoteFile;
/**
* The init operation initializes a new repository at a given remote storage
* location. Its responsibilities include:
*
* <ul>
* <li>Generating a master key from the user password (if encryption is enabled)
* using the {@link CipherUtil#createMasterKey(String) createMasterKey()} method</li>
* <li>Creating the local Syncany folder structure in the local directory (.syncany
* folder and the sub-structure).</li>
* <li>Initializing the remote storage (creating folder-structure, if necessary)
* using a transfer manager.</li>
* <li>Creating a new repo and master file using {@link RepoTO} and {@link MasterTO},
* saving them locally and uploading them to the remote repository.</li>
* </ul>
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class InitOperation extends AbstractInitOperation {
public static final String DEFAULT_IGNORE_FILE = "/" + InitOperation.class.getPackage().getName().replace('.', '/') + "/default.syignore";
private final InitOperationOptions options;
private final InitOperationResult result;
private TransferManager transferManager;
public InitOperation(InitOperationOptions options, UserInteractionListener listener) {
super(null, listener);
this.options = options;
this.result = new InitOperationResult();
}
@Override
public InitOperationResult execute() throws Exception {
logger.log(Level.INFO, "");
logger.log(Level.INFO, "Running 'Init'");
logger.log(Level.INFO, "--------------------------------------------");
transferManager = createTransferManagerFromNullConfig(options.getConfigTO());
// Test the repo
if (!performRepoTest()) {
logger.log(Level.INFO, "- Connecting to the repo failed, repo already exists or cannot be created: " + result.getResultCode());
return result;
}
logger.log(Level.INFO, "- Connecting to the repo was successful");
// Ask password (if needed)
String masterKeyPassword = null;
if (options.isEncryptionEnabled()) {
masterKeyPassword = getOrAskPassword();
}
// Create local .syncany directory
File appDir = createAppDirs(options.getLocalDir()); // TODO [medium] create temp dir first, ask password cannot be done after
File configFile = new File(appDir, Config.FILE_CONFIG);
File repoFile = new File(appDir, Config.FILE_REPO);
File masterFile = new File(appDir, Config.FILE_MASTER);
// Save config.xml and repo file
saveLocalConfig(configFile, repoFile, masterFile, masterKeyPassword);
// Make remote changes
logger.log(Level.INFO, "Uploading local repository ...");
makeRemoteChanges(configFile, masterFile, repoFile);
// Shutdown plugin
transferManager.disconnect();
// Add to daemon (if requested)
addToDaemonIfEnabled();
createDefaultIgnoreFile();
// Make link
GenlinkOperationResult genlinkOperationResult = generateLink(options.getConfigTO());
result.setResultCode(InitResultCode.OK);
result.setGenLinkResult(genlinkOperationResult);
return result;
}
private void createDefaultIgnoreFile() throws IOException {
try {
File ignoreFile = new File(options.getLocalDir(), Config.FILE_IGNORE);
logger.log(Level.INFO, "Creating default .syignore file at " + ignoreFile + " ...");
InputStream defaultConfigFileinputStream = InitOperation.class.getResourceAsStream(DEFAULT_IGNORE_FILE);
Files.copy(defaultConfigFileinputStream, ignoreFile.toPath());
}
catch (IOException e) {
logger.log(Level.WARNING, "Error creating default .syignore file. IGNORING.", e);
}
}
private void saveLocalConfig(File configFile, File repoFile, File masterFile, String masterKeyPassword) throws Exception {
if (options.isEncryptionEnabled()) {
SaltedSecretKey masterKey = createMasterKeyFromPassword(masterKeyPassword); // This takes looong!
options.getConfigTO().setMasterKey(masterKey);
new MasterTO(masterKey.getSalt()).save(masterFile);
options.getRepoTO().save(repoFile, options.getCipherSpecs(), masterKey);
}
else {
options.getRepoTO().save(repoFile);
}
options.getConfigTO().save(configFile);
}
private void makeRemoteChanges(File configFile, File masterFile, File repoFile) throws Exception {
initRemoteRepository(configFile);
try {
if (options.isEncryptionEnabled()) {
uploadMasterFile(masterFile, transferManager);
}
uploadRepoFile(repoFile, transferManager);
}
catch (StorageException | IOException e) {
cleanLocalRepository(e);
}
}
private void addToDaemonIfEnabled() {
if (options.isDaemon()) {
try {
boolean addedToDaemonConfig = DaemonConfigHelper.addFolder(options.getLocalDir());
result.setAddedToDaemon(addedToDaemonConfig);
}
catch (Exception e) {
logger.log(Level.WARNING, "Cannot add folder to daemon config.", e);
result.setAddedToDaemon(false);
}
}
}
private boolean performRepoTest() {
boolean testCreateTarget = options.isCreateTarget();
StorageTestResult testResult = transferManager.test(testCreateTarget);
logger.log(Level.INFO, "Storage test result ist " + testResult);
if (testResult.isTargetExists() && testResult.isTargetCanWrite() && !testResult.isRepoFileExists()) {
logger.log(Level.INFO, "--> OKAY: Target exists and is writable, but repo doesn't exist. We're good to go!");
return true;
}
else if (testCreateTarget && !testResult.isTargetExists() && testResult.isTargetCanCreate()) {
logger.log(Level.INFO, "--> OKAY: Target does not exist, but can be created. We're good to go!");
return true;
}
else {
logger.log(Level.INFO, "--> NOT OKAY: Invalid target/repo state. Operation cannot be continued.");
result.setResultCode(InitResultCode.NOK_TEST_FAILED);
result.setTestResult(testResult);
return false;
}
}
private void initRemoteRepository(File configFile) throws Exception {
try {
// Create 'syncany' and 'master' file, and all the remote folders
transferManager.init(options.isCreateTarget());
// Some plugins change the transfer settings, re-save
options.getConfigTO().save(configFile);
}
catch (StorageException e) {
// Storing remotely failed. Remove all the directories and files we just created
cleanLocalRepository(e);
}
}
private void cleanLocalRepository(Exception e) throws Exception {
try {
deleteAppDirs(options.getLocalDir());
}
catch (Exception e1) {
throw new StorageException("Couldn't upload to remote repo. Cleanup failed. There may be local directories left");
}
throw new StorageException("Couldn't upload to remote repo. Cleaned local repository.", e);
}
private GenlinkOperationResult generateLink(ConfigTO configTO) throws Exception {
return new GenlinkOperation(options.getConfigTO(), options.getGenlinkOptions()).execute();
}
private String getOrAskPassword() throws Exception {
if (options.getPassword() == null) {
if (listener == null) {
throw new RuntimeException("Cannot get password from user interface. No listener.");
}
return listener.onUserNewPassword();
}
else {
return options.getPassword();
}
}
private SaltedSecretKey createMasterKeyFromPassword(String masterPassword) throws Exception {
fireNotifyCreateMaster();
SaltedSecretKey masterKey = CipherUtil.createMasterKey(masterPassword);
return masterKey;
}
private void uploadMasterFile(File masterFile, TransferManager transferManager) throws Exception {
transferManager.upload(masterFile, new MasterRemoteFile());
}
private void uploadRepoFile(File repoFile, TransferManager transferManager) throws Exception {
transferManager.upload(repoFile, new SyncanyRemoteFile());
}
}