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}