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.config;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.IOException;
023import java.util.logging.Level;
024import java.util.logging.Logger;
025
026import org.simpleframework.xml.core.Persister;
027import org.syncany.config.to.ConfigTO;
028import org.syncany.config.to.RepoTO;
029import org.syncany.crypto.CipherUtil;
030import org.syncany.crypto.SaltedSecretKey;
031import org.syncany.plugins.Plugins;
032import org.syncany.plugins.transfer.TransferPlugin;
033
034/**
035 * The config helper provides convenience functions to load the configuration from
036 * the local application repo.
037 *
038 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
039 */
040public class ConfigHelper {
041        private static final Logger logger = Logger.getLogger(ConfigHelper.class.getSimpleName());
042
043        /**
044         * Loads a {@link Config} object from the given local directory.
045         *
046         * <p>If the config file (.syncany/config.xml) does not exist, <code>null</code>
047         * is returned. If it does, the method tries to do the following:
048         * <ul>
049         *  <li>Load the .syncany/config.xml file and load the plugin given by the config file</li>
050         *  <li>Read .syncany/repo, decrypt it using the master key (if necessary) and load it</li>
051         *  <li>Instantiate a {@link Config} object with the transfer objects</li>
052         * </ul>
053         *
054         * @return Returns an instantiated {@link Config} object, or <code>null</code> if
055         *         the config file does not exist
056         * @throws ConfigException an exception if the config is invalid
057         */
058        public static Config loadConfig(File localDir) throws ConfigException {
059                if (localDir == null) {
060                        throw new ConfigException("Argument localDir cannot be null.");
061                }
062
063                File appDir = new File(localDir, Config.DIR_APPLICATION);
064
065                if (appDir.exists()) {
066                        logger.log(Level.INFO, "Loading config from {0} ...", localDir);
067
068                        ConfigTO configTO = ConfigHelper.loadConfigTO(localDir);
069                        RepoTO repoTO = ConfigHelper.loadRepoTO(localDir, configTO);
070
071                        String pluginId = (configTO.getTransferSettings() != null) ? configTO.getTransferSettings().getType() : null;
072                        TransferPlugin plugin = Plugins.get(pluginId, TransferPlugin.class);
073
074                        if (plugin == null) {
075                                logger.log(Level.WARNING, "Not loading config! Plugin with id '{0}' does not exist.", pluginId);
076                                throw new ConfigException("Plugin with id '" + pluginId + "' does not exist. Try 'sy plugin install " + pluginId + "'.");
077                        }
078
079                        logger.log(Level.INFO, "Initializing Config instance ...");
080                        return new Config(localDir, configTO, repoTO);
081                }
082                else {
083                        logger.log(Level.INFO, "Not loading config, app dir does not exist: {0}", appDir);
084                        return null;
085                }
086        }
087
088        /**
089         * Returns true if the config.xml file exists, given a local directory.
090         */
091        public static boolean configExists(File localDir) {
092                File appDir = new File(localDir, Config.DIR_APPLICATION);
093                File configFile = new File(appDir, Config.FILE_CONFIG);
094
095                return configFile.exists();
096        }
097
098        /**
099         * Loads the config transfer object from the local directory
100         * or throws an exception if the file does not exist.
101         */
102    public static ConfigTO loadConfigTO(File localDir) throws ConfigException {
103        File appDir = new File(localDir, Config.DIR_APPLICATION);
104                File configFile = new File(appDir, Config.FILE_CONFIG);
105
106                if (!configFile.exists()) {
107                        throw new ConfigException("Cannot find config file at "+configFile+". Try connecting to a repository using 'connect', or 'init' to create a new one.");
108                }
109
110                return ConfigTO.load(configFile);
111        }
112
113    /**
114     * Loads the repository transfer object from the local directory.
115     */
116    public static RepoTO loadRepoTO(File localDir, ConfigTO configTO) throws ConfigException {
117        File appDir = new File(localDir, Config.DIR_APPLICATION);
118                File repoFile = new File(appDir, Config.FILE_REPO);
119
120                if (!repoFile.exists()) {
121                        throw new ConfigException("Cannot find repository file at "+repoFile+". Try connecting to a repository using 'connect', or 'init' to create a new one.");
122                }
123
124                try {
125                        if (CipherUtil.isEncrypted(repoFile)) {
126                                return loadEncryptedRepoTO(repoFile, configTO);
127                        }
128                        else {
129                                return loadPlaintextRepoTO(repoFile, configTO);
130                        }
131                }
132                catch (Exception e) {
133                        throw new ConfigException("Cannot load repo file: "+e.getMessage(), e);
134                }
135        }
136
137    /**
138     * Helper method to find the local sync directory, starting from a path equal
139     * or inside the local sync directory. If the starting path is not inside or equal
140     * to the local directory, <code>null</code> is returned.
141     *
142     * <p>To find the local directory, the method looks for a file named
143     * "{@link Config#DIR_APPLICATION}/{@link Config#FILE_CONFIG}". If it is found, it stops.
144     * If not, it continues looking in the parent directory.
145     *
146     * <p>Example: If /home/user/Syncany is the local sync directory and /home/user/NotSyncany
147     * is not a local directory, the method will return the following:
148     *
149     * <ul>
150     *  <li>findLocalDirInPath(/home/user/Syncany) -&gt; /home/user/Syncany</li>
151     *  <li>findLocalDirInPath(/home/user/Syncany/some/subfolder) -&gt; /home/user/Syncany</li>
152     *  <li>findLocalDirInPath(/home/user/NotSyncany) -&gt;null</li>
153     * </ul>
154     *
155     * @param startingPath Path to start the search from
156     * @return Returns the local directory (if found), or <code>null</code> otherwise
157     */
158    public static File findLocalDirInPath(File startingPath) {
159        try {
160                        File currentSearchFolder = startingPath.getCanonicalFile();
161
162                        while (currentSearchFolder != null) {
163                                File possibleAppDir = new File(currentSearchFolder, Config.DIR_APPLICATION);
164                                File possibleConfigFile = new File(possibleAppDir, Config.FILE_CONFIG);
165
166                                if (possibleAppDir.exists() && possibleConfigFile.exists()) {
167                                        return possibleAppDir.getParentFile().getCanonicalFile();
168                                }
169
170                                currentSearchFolder = currentSearchFolder.getParentFile();
171                        }
172
173                        return null;
174        }
175        catch (IOException e) {
176                throw new RuntimeException("Unable to determine local directory starting from: "+startingPath, e);
177        }
178        }
179
180    private static RepoTO loadPlaintextRepoTO(File repoFile, ConfigTO configTO) throws Exception {
181        logger.log(Level.INFO, "Loading (unencrypted) repo file from {0} ...", repoFile);
182                return new Persister().read(RepoTO.class, repoFile);
183    }
184
185    private static RepoTO loadEncryptedRepoTO(File repoFile, ConfigTO configTO) throws Exception {
186        logger.log(Level.INFO, "Loading encrypted repo file from {0} ...", repoFile);
187
188                SaltedSecretKey masterKey = configTO.getMasterKey();
189
190                if (masterKey == null) {
191                        throw new ConfigException("Repo file is encrypted, but master key not set in config file.");
192                }
193
194                String repoFileStr = new String(CipherUtil.decrypt(new FileInputStream(repoFile), masterKey));
195                return new Persister().read(RepoTO.class, repoFileStr);
196    }
197}