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) -> /home/user/Syncany</li> 151 * <li>findLocalDirInPath(/home/user/Syncany/some/subfolder) -> /home/user/Syncany</li> 152 * <li>findLocalDirInPath(/home/user/NotSyncany) ->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}