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.util.ArrayList;
022import java.util.List;
023
024import org.syncany.chunk.Chunker;
025import org.syncany.chunk.CipherTransformer;
026import org.syncany.chunk.FixedChunker;
027import org.syncany.chunk.MultiChunker;
028import org.syncany.chunk.NoTransformer;
029import org.syncany.chunk.Transformer;
030import org.syncany.config.to.ConfigTO;
031import org.syncany.config.to.RepoTO;
032import org.syncany.config.to.RepoTO.MultiChunkerTO;
033import org.syncany.config.to.RepoTO.TransformerTO;
034import org.syncany.crypto.SaltedSecretKey;
035import org.syncany.database.DatabaseConnectionFactory;
036import org.syncany.database.VectorClock;
037import org.syncany.plugins.Plugins;
038import org.syncany.plugins.transfer.TransferPlugin;
039import org.syncany.plugins.transfer.TransferSettings;
040import org.syncany.util.FileUtil;
041import org.syncany.util.StringUtil;
042
043/**
044 * The config class is the central point to configure a Syncany instance. It is mainly
045 * used in the operations, but parts of it are also used in other parts of the
046 * application -- especially file locations and names.
047 *
048 * <p>An instance of the <code>Config</code> class must be created through the transfer
049 * objects {@link ConfigTO} and {@link RepoTO}.
050 *
051 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
052 */
053public class Config {
054        public static final String DIR_APPLICATION = ".syncany";
055        public static final String DIR_CACHE = "cache";
056        public static final String DIR_DATABASE = "db";
057        public static final String DIR_LOG = "logs";
058        public static final String DIR_STATE = "state";
059
060        // File in managed folder root
061        public static final String FILE_IGNORE = ".syignore";
062
063        // Files in .syncany
064        public static final String FILE_CONFIG = "config.xml";
065        public static final String FILE_REPO = "syncany";
066        public static final String FILE_MASTER = "master";
067
068        // File in .syncany/db
069        public static final String FILE_DATABASE = "local.db";
070
071        // Files in .syncany/state
072        public static final String FILE_PORT = "port.xml";
073        public static final String FILE_TRANSACTION = "transaction-actions.xml";
074        public static final String FILE_TRANSACTION_DATABASE = "transaction-database.xml";
075        public static final String FILE_TRANSACTION_PATTERN = "transaction-actions.%010d.xml";
076        public static final String FILE_TRANSACTION_DATABASE_PATTERN = "transaction-database.%010d.xml";
077        public static final String FILE_TRANSACTION_LIST = "transaction-list.txt";
078
079        private byte[] repoId;
080        private String machineName;
081        private String displayName;
082        private File localDir;
083        private File appDir;
084        private File cacheDir;
085        private File databaseDir;
086        private File logDir;
087        private File stateDir;
088
089        private SaltedSecretKey masterKey;
090
091        private Cache cache;
092        private TransferPlugin plugin;
093        private TransferSettings transferSettings;
094        private Chunker chunker;
095        private MultiChunker multiChunker;
096        private Transformer transformer;
097        private IgnoredFiles ignoredFiles;
098
099        static {
100                UserConfig.init();
101                Logging.init();
102        }
103
104        public Config(File aLocalDir, ConfigTO configTO, RepoTO repoTO) throws ConfigException {
105                if (aLocalDir == null || configTO == null || repoTO == null) {
106                        throw new ConfigException("Arguments aLocalDir, configTO and repoTO cannot be null.");
107                }
108
109                initNames(configTO);
110                initMasterKey(configTO);
111                initDirectories(aLocalDir);
112                initCache(configTO);
113                initIgnoredFile();
114                initRepo(repoTO);
115                initConnection(configTO);
116        }
117
118        private void initNames(ConfigTO configTO) throws ConfigException {
119                setMachineName(configTO.getMachineName());
120                setDisplayName(configTO.getDisplayName());
121        }
122
123        private void initMasterKey(ConfigTO configTO) {
124                masterKey = configTO.getMasterKey(); // can be null
125        }
126
127        private void initDirectories(File aLocalDir) throws ConfigException {
128                localDir = FileUtil.getCanonicalFile(aLocalDir);
129                appDir = FileUtil.getCanonicalFile(new File(localDir, DIR_APPLICATION));
130                cacheDir = FileUtil.getCanonicalFile(new File(appDir, DIR_CACHE));
131                databaseDir = FileUtil.getCanonicalFile(new File(appDir, DIR_DATABASE));
132                logDir = FileUtil.getCanonicalFile(new File(appDir, DIR_LOG));
133                stateDir = FileUtil.getCanonicalFile(new File(appDir, DIR_STATE));
134        }
135
136        private void initCache(ConfigTO configTO) {
137                cache = new Cache(cacheDir);
138
139                if (configTO.getCacheKeepBytes() != null && configTO.getCacheKeepBytes() >= 0) {
140                        cache.setKeepBytes(configTO.getCacheKeepBytes());
141                }
142        }
143
144        private void initIgnoredFile() throws ConfigException {
145                File ignoreFile = new File(localDir, FILE_IGNORE);
146                ignoredFiles = new IgnoredFiles(ignoreFile);
147        }
148
149        private void initRepo(RepoTO repoTO) throws ConfigException {
150                try {
151                        initRepoId(repoTO);
152                        initChunker(repoTO);
153                        initMultiChunker(repoTO);
154                        initTransformers(repoTO);
155                }
156                catch (Exception e) {
157                        throw new ConfigException("Unable to initialize repository information from config.", e);
158                }
159        }
160
161        private void initRepoId(RepoTO repoTO) {
162                repoId = repoTO.getRepoId();
163        }
164
165        private void initChunker(RepoTO repoTO) throws Exception {
166                // TODO [feature request] make chunking options configurable, something like described in #29
167                // See: https://github.com/syncany/syncany/issues/29#issuecomment-43425647
168
169                chunker = new FixedChunker(512 * 1024, "SHA1");
170        }
171
172        private void initMultiChunker(RepoTO repoTO) throws ConfigException {
173                MultiChunkerTO multiChunkerTO = repoTO.getMultiChunker();
174
175                if (multiChunkerTO == null) {
176                        throw new ConfigException("No multichunker in repository config.");
177                }
178
179                multiChunker = MultiChunker.getInstance(multiChunkerTO.getType());
180
181                if (multiChunker == null) {
182                        throw new ConfigException("Invalid multichunk type or settings: " + multiChunkerTO.getType());
183                }
184
185                multiChunker.init(multiChunkerTO.getSettings());
186        }
187
188        private void initTransformers(RepoTO repoTO) throws Exception {
189                if (repoTO.getTransformers() == null || repoTO.getTransformers().size() == 0) {
190                        transformer = new NoTransformer();
191                }
192                else {
193                        List<TransformerTO> transformerTOs = new ArrayList<TransformerTO>(repoTO.getTransformers());
194                        Transformer lastTransformer = null;
195
196                        for (int i = transformerTOs.size() - 1; i >= 0; i--) {
197                                TransformerTO transformerTO = transformerTOs.get(i);
198                                Transformer transformer = Transformer.getInstance(transformerTO.getType());
199
200                                if (transformer == null) {
201                                        throw new ConfigException("Cannot find transformer '" + transformerTO.getType() + "'");
202                                }
203
204                                if (transformer instanceof CipherTransformer) { // Dirty workaround
205                                        transformerTO.getSettings().put(CipherTransformer.PROPERTY_MASTER_KEY, StringUtil.toHex(getMasterKey().getEncoded()));
206                                        transformerTO.getSettings().put(CipherTransformer.PROPERTY_MASTER_KEY_SALT, StringUtil.toHex(getMasterKey().getSalt()));
207                                }
208
209                                transformer.init(transformerTO.getSettings());
210
211                                if (lastTransformer != null) {
212                                        transformer.setNextTransformer(lastTransformer);
213                                }
214
215                                lastTransformer = transformer;
216                        }
217
218                        transformer = lastTransformer;
219                }
220        }
221
222        private void initConnection(ConfigTO configTO) throws ConfigException {
223                if (configTO.getTransferSettings() != null) {
224                        plugin = Plugins.get(configTO.getTransferSettings().getType(), TransferPlugin.class);
225
226                        if (plugin == null) {
227                                throw new ConfigException("Plugin not supported: " + configTO.getTransferSettings().getType());
228                        }
229
230                        try {
231                                transferSettings = configTO.getTransferSettings();
232                        }
233                        catch (Exception e) {
234                                throw new ConfigException("Cannot initialize storage: " + e.getMessage(), e);
235                        }
236                }
237        }
238
239        public java.sql.Connection createDatabaseConnection() {
240                return DatabaseConnectionFactory.createConnection(getDatabaseFile(), false);
241        }
242
243        public java.sql.Connection createDatabaseConnection(boolean readOnly) {
244                return DatabaseConnectionFactory.createConnection(getDatabaseFile(), readOnly);
245        }
246
247        public File getCacheDir() {
248                return cacheDir;
249        }
250
251        public File getAppDir() {
252                return appDir;
253        }
254
255        public String getMachineName() {
256                return machineName;
257        }
258
259        public void setMachineName(String machineName) throws ConfigException {
260                if (machineName == null || !VectorClock.MACHINE_PATTERN.matcher(machineName).matches()) {
261                        throw new ConfigException("Machine name cannot be empty and must be only characters (A-Z).");
262                }
263
264                this.machineName = machineName;
265        }
266
267        public String getDisplayName() {
268                return displayName;
269        }
270
271        public void setDisplayName(String displayName) {
272                this.displayName = displayName;
273        }
274
275        public TransferPlugin getTransferPlugin() {
276                return plugin;
277        }
278
279        public TransferSettings getConnection() {
280                return transferSettings;
281        }
282
283        public void setConnection(TransferSettings connection) {
284                transferSettings = connection;
285        }
286
287        public byte[] getRepoId() {
288                return repoId;
289        }
290
291        public Chunker getChunker() {
292                return chunker;
293        }
294
295        public Cache getCache() {
296                return cache;
297        }
298
299        public IgnoredFiles getIgnoredFiles() {
300                return ignoredFiles;
301        }
302
303        public MultiChunker getMultiChunker() {
304                return multiChunker;
305        }
306
307        public Transformer getTransformer() {
308                return transformer;
309        }
310
311        public void setCache(Cache cache) {
312                this.cache = cache;
313        }
314
315        public File getLocalDir() {
316                return localDir;
317        }
318
319        public File getDatabaseDir() {
320                return databaseDir;
321        }
322
323        public File getLogDir() {
324                return logDir;
325        }
326
327        public File getStateDir() {
328                return stateDir;
329        }
330
331        public SaltedSecretKey getMasterKey() {
332                return masterKey;
333        }
334
335        public File getDatabaseFile() {
336                return new File(databaseDir, FILE_DATABASE);
337        }
338
339        public File getPortFile() {
340                return new File(stateDir, FILE_PORT);
341        }
342
343        public File getTransactionFile() {
344                return new File(stateDir, FILE_TRANSACTION);
345        }
346
347        public File getTransactionDatabaseFile() {
348                return new File(stateDir, FILE_TRANSACTION_DATABASE);
349        }
350
351        public File getTransactionListFile() {
352                return new File(stateDir, FILE_TRANSACTION_LIST);
353        }
354
355        public File getTransactionFile(long databaseVersionNumber) {
356                return new File(stateDir, String.format(FILE_TRANSACTION_PATTERN, databaseVersionNumber));
357        }
358
359        public File getTransactionDatabaseFile(long databaseVersionNumber) {
360                return new File(stateDir, String.format(FILE_TRANSACTION_DATABASE_PATTERN, databaseVersionNumber));
361        }
362}