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.init; 019 020import java.io.File; 021import java.io.IOException; 022import java.io.InputStream; 023import java.nio.file.Files; 024import java.util.logging.Level; 025 026import org.syncany.config.Config; 027import org.syncany.config.DaemonConfigHelper; 028import org.syncany.config.to.ConfigTO; 029import org.syncany.config.to.MasterTO; 030import org.syncany.config.to.RepoTO; 031import org.syncany.crypto.CipherUtil; 032import org.syncany.crypto.SaltedSecretKey; 033import org.syncany.operations.init.InitOperationResult.InitResultCode; 034import org.syncany.plugins.UserInteractionListener; 035import org.syncany.plugins.transfer.StorageException; 036import org.syncany.plugins.transfer.StorageTestResult; 037import org.syncany.plugins.transfer.TransferManager; 038import org.syncany.plugins.transfer.files.MasterRemoteFile; 039import org.syncany.plugins.transfer.files.SyncanyRemoteFile; 040 041/** 042 * The init operation initializes a new repository at a given remote storage 043 * location. Its responsibilities include: 044 * 045 * <ul> 046 * <li>Generating a master key from the user password (if encryption is enabled) 047 * using the {@link CipherUtil#createMasterKey(String) createMasterKey()} method</li> 048 * <li>Creating the local Syncany folder structure in the local directory (.syncany 049 * folder and the sub-structure).</li> 050 * <li>Initializing the remote storage (creating folder-structure, if necessary) 051 * using a transfer manager.</li> 052 * <li>Creating a new repo and master file using {@link RepoTO} and {@link MasterTO}, 053 * saving them locally and uploading them to the remote repository.</li> 054 * </ul> 055 * 056 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 057 */ 058public class InitOperation extends AbstractInitOperation { 059 public static final String DEFAULT_IGNORE_FILE = "/" + InitOperation.class.getPackage().getName().replace('.', '/') + "/default.syignore"; 060 061 private final InitOperationOptions options; 062 private final InitOperationResult result; 063 064 private TransferManager transferManager; 065 066 public InitOperation(InitOperationOptions options, UserInteractionListener listener) { 067 super(null, listener); 068 069 this.options = options; 070 this.result = new InitOperationResult(); 071 } 072 073 @Override 074 public InitOperationResult execute() throws Exception { 075 logger.log(Level.INFO, ""); 076 logger.log(Level.INFO, "Running 'Init'"); 077 logger.log(Level.INFO, "--------------------------------------------"); 078 079 transferManager = createTransferManagerFromNullConfig(options.getConfigTO()); 080 081 // Test the repo 082 if (!performRepoTest()) { 083 logger.log(Level.INFO, "- Connecting to the repo failed, repo already exists or cannot be created: " + result.getResultCode()); 084 return result; 085 } 086 087 logger.log(Level.INFO, "- Connecting to the repo was successful"); 088 089 // Ask password (if needed) 090 String masterKeyPassword = null; 091 092 if (options.isEncryptionEnabled()) { 093 masterKeyPassword = getOrAskPassword(); 094 } 095 096 // Create local .syncany directory 097 File appDir = createAppDirs(options.getLocalDir()); // TODO [medium] create temp dir first, ask password cannot be done after 098 File configFile = new File(appDir, Config.FILE_CONFIG); 099 File repoFile = new File(appDir, Config.FILE_REPO); 100 File masterFile = new File(appDir, Config.FILE_MASTER); 101 102 // Save config.xml and repo file 103 saveLocalConfig(configFile, repoFile, masterFile, masterKeyPassword); 104 105 // Make remote changes 106 logger.log(Level.INFO, "Uploading local repository ..."); 107 makeRemoteChanges(configFile, masterFile, repoFile); 108 109 // Shutdown plugin 110 transferManager.disconnect(); 111 112 // Add to daemon (if requested) 113 addToDaemonIfEnabled(); 114 createDefaultIgnoreFile(); 115 116 // Make link 117 GenlinkOperationResult genlinkOperationResult = generateLink(options.getConfigTO()); 118 119 result.setResultCode(InitResultCode.OK); 120 result.setGenLinkResult(genlinkOperationResult); 121 122 return result; 123 } 124 125 private void createDefaultIgnoreFile() throws IOException { 126 try { 127 File ignoreFile = new File(options.getLocalDir(), Config.FILE_IGNORE); 128 129 logger.log(Level.INFO, "Creating default .syignore file at " + ignoreFile + " ..."); 130 131 InputStream defaultConfigFileinputStream = InitOperation.class.getResourceAsStream(DEFAULT_IGNORE_FILE); 132 Files.copy(defaultConfigFileinputStream, ignoreFile.toPath()); 133 } 134 catch (IOException e) { 135 logger.log(Level.WARNING, "Error creating default .syignore file. IGNORING.", e); 136 } 137 } 138 139 private void saveLocalConfig(File configFile, File repoFile, File masterFile, String masterKeyPassword) throws Exception { 140 if (options.isEncryptionEnabled()) { 141 SaltedSecretKey masterKey = createMasterKeyFromPassword(masterKeyPassword); // This takes looong! 142 options.getConfigTO().setMasterKey(masterKey); 143 144 new MasterTO(masterKey.getSalt()).save(masterFile); 145 options.getRepoTO().save(repoFile, options.getCipherSpecs(), masterKey); 146 } 147 else { 148 options.getRepoTO().save(repoFile); 149 } 150 151 options.getConfigTO().save(configFile); 152 } 153 154 private void makeRemoteChanges(File configFile, File masterFile, File repoFile) throws Exception { 155 initRemoteRepository(configFile); 156 157 try { 158 if (options.isEncryptionEnabled()) { 159 uploadMasterFile(masterFile, transferManager); 160 } 161 162 uploadRepoFile(repoFile, transferManager); 163 } 164 catch (StorageException | IOException e) { 165 cleanLocalRepository(e); 166 } 167 } 168 169 private void addToDaemonIfEnabled() { 170 if (options.isDaemon()) { 171 try { 172 boolean addedToDaemonConfig = DaemonConfigHelper.addFolder(options.getLocalDir()); 173 result.setAddedToDaemon(addedToDaemonConfig); 174 } 175 catch (Exception e) { 176 logger.log(Level.WARNING, "Cannot add folder to daemon config.", e); 177 result.setAddedToDaemon(false); 178 } 179 } 180 } 181 182 private boolean performRepoTest() { 183 boolean testCreateTarget = options.isCreateTarget(); 184 StorageTestResult testResult = transferManager.test(testCreateTarget); 185 186 logger.log(Level.INFO, "Storage test result ist " + testResult); 187 188 if (testResult.isTargetExists() && testResult.isTargetCanWrite() && !testResult.isRepoFileExists()) { 189 logger.log(Level.INFO, "--> OKAY: Target exists and is writable, but repo doesn't exist. We're good to go!"); 190 return true; 191 } 192 else if (testCreateTarget && !testResult.isTargetExists() && testResult.isTargetCanCreate()) { 193 logger.log(Level.INFO, "--> OKAY: Target does not exist, but can be created. We're good to go!"); 194 return true; 195 } 196 else { 197 logger.log(Level.INFO, "--> NOT OKAY: Invalid target/repo state. Operation cannot be continued."); 198 199 result.setResultCode(InitResultCode.NOK_TEST_FAILED); 200 result.setTestResult(testResult); 201 202 return false; 203 } 204 } 205 206 private void initRemoteRepository(File configFile) throws Exception { 207 try { 208 // Create 'syncany' and 'master' file, and all the remote folders 209 transferManager.init(options.isCreateTarget()); 210 211 // Some plugins change the transfer settings, re-save 212 options.getConfigTO().save(configFile); 213 } 214 catch (StorageException e) { 215 // Storing remotely failed. Remove all the directories and files we just created 216 cleanLocalRepository(e); 217 } 218 } 219 220 private void cleanLocalRepository(Exception e) throws Exception { 221 try { 222 deleteAppDirs(options.getLocalDir()); 223 } 224 catch (Exception e1) { 225 throw new StorageException("Couldn't upload to remote repo. Cleanup failed. There may be local directories left"); 226 } 227 228 throw new StorageException("Couldn't upload to remote repo. Cleaned local repository.", e); 229 } 230 231 private GenlinkOperationResult generateLink(ConfigTO configTO) throws Exception { 232 return new GenlinkOperation(options.getConfigTO(), options.getGenlinkOptions()).execute(); 233 } 234 235 private String getOrAskPassword() throws Exception { 236 if (options.getPassword() == null) { 237 if (listener == null) { 238 throw new RuntimeException("Cannot get password from user interface. No listener."); 239 } 240 241 return listener.onUserNewPassword(); 242 } 243 else { 244 return options.getPassword(); 245 } 246 } 247 248 private SaltedSecretKey createMasterKeyFromPassword(String masterPassword) throws Exception { 249 fireNotifyCreateMaster(); 250 251 SaltedSecretKey masterKey = CipherUtil.createMasterKey(masterPassword); 252 return masterKey; 253 } 254 255 private void uploadMasterFile(File masterFile, TransferManager transferManager) throws Exception { 256 transferManager.upload(masterFile, new MasterRemoteFile()); 257 } 258 259 private void uploadRepoFile(File repoFile, TransferManager transferManager) throws Exception { 260 transferManager.upload(repoFile, new SyncanyRemoteFile()); 261 } 262}