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.plugins.local;
019
020import java.io.File;
021import java.io.IOException;
022import java.nio.file.DirectoryStream;
023import java.nio.file.Files;
024import java.nio.file.Path;
025import java.nio.file.Paths;
026import java.util.Map;
027import java.util.logging.Level;
028import java.util.logging.Logger;
029
030import org.apache.commons.io.FileUtils;
031import org.syncany.config.Config;
032import org.syncany.plugins.transfer.AbstractTransferManager;
033import org.syncany.plugins.transfer.StorageException;
034import org.syncany.plugins.transfer.StorageFileNotFoundException;
035import org.syncany.plugins.transfer.StorageMoveException;
036import org.syncany.plugins.transfer.TransferManager;
037import org.syncany.plugins.transfer.files.ActionRemoteFile;
038import org.syncany.plugins.transfer.files.CleanupRemoteFile;
039import org.syncany.plugins.transfer.files.DatabaseRemoteFile;
040import org.syncany.plugins.transfer.files.MultichunkRemoteFile;
041import org.syncany.plugins.transfer.files.RemoteFile;
042import org.syncany.plugins.transfer.files.SyncanyRemoteFile;
043import org.syncany.plugins.transfer.files.TempRemoteFile;
044import org.syncany.plugins.transfer.files.TransactionRemoteFile;
045
046import com.google.common.collect.Maps;
047
048/**
049 * Implements a {@link TransferManager} based on a local storage backend for the
050 * {@link LocalTransferPlugin}.
051 *
052 * <p>Using a {@link LocalTransferSettings}, the transfer manager is configured and uses
053 * any local folder to store the Syncany repository data. While repo and
054 * master file are stored in the given folder, databases and multichunks are stored
055 * in special sub-folders:
056 *
057 * <ul>
058 * <li>The <code>databases</code> folder keeps all the {@link DatabaseRemoteFile}s</li>
059 * <li>The <code>multichunks</code> folder keeps the actual data within the {@link MultichunkRemoteFile}s</li>
060 * </ul>
061 *
062 * <p>This plugin can be used for testing or to point to a repository
063 * on a mounted remote device or network storage such as an NFS or a
064 * Samba/NetBIOS share.
065 *
066 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
067 */
068public class LocalTransferManager extends AbstractTransferManager {
069        private static final Logger logger = Logger.getLogger(LocalTransferManager.class.getSimpleName());
070
071        private Path repoPath;
072        private Path multichunksPath;
073        private Path databasesPath;
074        private Path actionsPath;
075        private Path transactionsPath;
076        private Path temporaryPath;
077
078        public LocalTransferManager(LocalTransferSettings connection, Config config) {
079                super(connection, config);
080
081                this.repoPath = Paths.get(connection.getPath().toURI()); // absolute file to get abs. path!
082                this.multichunksPath = repoPath.resolve("multichunks");
083                this.databasesPath = repoPath.resolve("databases");
084                this.actionsPath = repoPath.resolve("actions");
085                this.transactionsPath = repoPath.resolve("transactions");
086                this.temporaryPath = repoPath.resolve("temporary");
087        }
088
089        @Override
090        public void connect() throws StorageException {
091                if (repoPath == null) {
092                        throw new StorageException("Repository folder '" + repoPath + "' does not exist or is not writable.");
093                }
094        }
095
096        @Override
097        public void disconnect() throws StorageException {
098                // Nothing.
099        }
100
101        @Override
102        public void init(boolean createIfRequired) throws StorageException {
103                connect();
104
105                try {
106                        if (!testTargetExists() && createIfRequired) {
107                                if (!Files.exists(Files.createDirectory(repoPath))) {
108                                        throw new StorageException("Cannot create repository directory: " + repoPath);
109                                }
110                        }
111
112                        if (!Files.exists(Files.createDirectory(multichunksPath))) {
113                                throw new StorageException("Cannot create multichunk directory: " + multichunksPath);
114                        }
115
116                        if (!Files.exists(Files.createDirectory(databasesPath))) {
117                                throw new StorageException("Cannot create databases directory: " + databasesPath);
118                        }
119
120                        if (!Files.exists(Files.createDirectory(actionsPath))) {
121                                throw new StorageException("Cannot create actions directory: " + actionsPath);
122                        }
123
124                        if (!Files.exists(Files.createDirectory(transactionsPath))) {
125                                throw new StorageException("Cannot create transactions directory: " + transactionsPath);
126                        }
127
128                        if (!Files.exists(Files.createDirectory(temporaryPath))) {
129                                throw new StorageException("Cannot create temporary directory: " + temporaryPath);
130                        }
131                }
132                catch (IOException e) {
133                        throw new StorageException("Unable to create directories", e);
134                }
135        }
136
137        @Override
138        public void download(RemoteFile remoteFile, File localFile) throws StorageException {
139                connect();
140
141                File repoFile = getRemoteFile(remoteFile);
142
143                if (!repoFile.exists()) {
144                        throw new StorageFileNotFoundException("No such file in local repository: " + repoFile);
145                }
146
147                try {
148                        File tempLocalFile = createTempFile("local-tm-download");
149                        tempLocalFile.deleteOnExit();
150
151                        FileUtils.copyFile(repoFile, tempLocalFile);
152
153                        localFile.delete();
154                        FileUtils.moveFile(tempLocalFile, localFile);
155                        tempLocalFile.delete();
156                }
157                catch (IOException ex) {
158                        throw new StorageException("Unable to copy file " + repoFile + " from local repository to " + localFile, ex);
159                }
160        }
161
162        @Override
163        public void move(RemoteFile sourceFile, RemoteFile targetFile) throws StorageException {
164                connect();
165
166                File sourceRemoteFile = getRemoteFile(sourceFile);
167                File targetRemoteFile = getRemoteFile(targetFile);
168
169                if (!sourceRemoteFile.exists()) {
170                        throw new StorageMoveException("Unable to move file " + sourceFile + " because it does not exist.");
171                }
172
173                try {
174                        FileUtils.moveFile(sourceRemoteFile, targetRemoteFile);
175                }
176                catch (IOException ex) {
177                        throw new StorageException("Unable to move file " + sourceRemoteFile + " to destination " + targetRemoteFile, ex);
178                }
179        }
180
181        @Override
182        public void upload(File localFile, RemoteFile remoteFile) throws StorageException {
183                connect();
184
185                File repoFile = getRemoteFile(remoteFile);
186                File tempRepoFile = new File(getAbsoluteParentDirectory(repoFile) + File.separator + ".temp-" + repoFile.getName());
187
188                // Do not overwrite files with same size!
189                if (repoFile.exists() && repoFile.length() == localFile.length()) {
190                        return;
191                }
192
193                // No such local file
194                if (!localFile.exists()) {
195                        throw new StorageException("No such file on local disk: " + localFile);
196                }
197
198                try {
199                        FileUtils.copyFile(localFile, tempRepoFile);
200                        FileUtils.moveFile(tempRepoFile, repoFile);
201                }
202                catch (IOException ex) {
203                        throw new StorageException("Unable to copy file " + localFile + " to local repository " + repoFile, ex);
204                }
205        }
206
207        @Override
208        public boolean delete(RemoteFile remoteFile) throws StorageException {
209                connect();
210
211                File repoFile = getRemoteFile(remoteFile);
212
213                return !repoFile.exists() || repoFile.delete();
214
215        }
216
217        @Override
218        public <T extends RemoteFile> Map<String, T> list(Class<T> remoteFileClass) throws StorageException {
219                connect();
220
221                Path folder = Paths.get(getRemoteFilePath(remoteFileClass));
222                Map<String, T> files = Maps.newHashMap();
223
224                try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(folder)) {
225                        for (Path path : directoryStream) {
226                                try {
227                                        T remoteFile = RemoteFile.createRemoteFile(path.getFileName().toString(), remoteFileClass);
228                                        files.put(path.getFileName().toString(), remoteFile);
229                                }
230                                catch (StorageException e) {
231                                        logger.log(Level.INFO, "Cannot create instance of " + remoteFileClass.getSimpleName() + " for file " + path
232                                                                        + "; maybe invalid file name pattern. Ignoring file.");
233                                }
234                        }
235                }
236                catch (IOException e) {
237                        logger.log(Level.SEVERE, "Unable to list directory", e);
238                }
239
240                return files;
241        }
242
243        @Override
244        public String getRemoteFilePath(Class<? extends RemoteFile> remoteFile) {
245                if (remoteFile.equals(MultichunkRemoteFile.class)) {
246                        return multichunksPath.toString();
247                }
248                else if (remoteFile.equals(DatabaseRemoteFile.class) || remoteFile.equals(CleanupRemoteFile.class)) {
249                        return databasesPath.toString();
250                }
251                else if (remoteFile.equals(ActionRemoteFile.class)) {
252                        return actionsPath.toString();
253                }
254                else if (remoteFile.equals(TransactionRemoteFile.class)) {
255                        return transactionsPath.toString();
256                }
257                else if (remoteFile.equals(TempRemoteFile.class)) {
258                        return temporaryPath.toString();
259                }
260                else {
261                        return repoPath.toString();
262                }
263        }
264
265        private File getRemoteFile(RemoteFile remoteFile) {
266                String rootPath = getRemoteFilePath(remoteFile.getClass());
267                return Paths.get(rootPath, remoteFile.getName()).toFile();
268        }
269
270        public String getAbsoluteParentDirectory(File file) {
271                return file.getAbsolutePath().substring(0, file.getAbsolutePath().lastIndexOf(File.separator));
272        }
273
274        @Override
275        public boolean testTargetCanWrite() {
276                try {
277                        if (Files.isDirectory(repoPath)) {
278                                Path tempFile = Files.createTempFile(repoPath, "syncany-write-test", "tmp");
279                                Files.delete(tempFile);
280
281                                logger.log(Level.INFO, "testTargetCanWrite: Can write, test file created/deleted successfully.");
282                                return true;
283                        }
284                        else {
285                                logger.log(Level.INFO, "testTargetCanWrite: Can NOT write, target does not exist.");
286                                return false;
287                        }
288                }
289                catch (Exception e) {
290                        logger.log(Level.INFO, "testTargetCanWrite: Can NOT write to target.", e);
291                        return false;
292                }
293        }
294
295        @Override
296        public boolean testTargetExists() {
297                if (Files.exists(repoPath)) {
298                        logger.log(Level.INFO, "testTargetExists: Target exists.");
299                        return true;
300                }
301                else {
302                        logger.log(Level.INFO, "testTargetExists: Target does NOT exist.");
303                        return false;
304                }
305        }
306
307        @Override
308        public boolean testRepoFileExists() {
309                try {
310                        File repoFile = getRemoteFile(new SyncanyRemoteFile());
311
312                        if (repoFile.exists()) {
313                                logger.log(Level.INFO, "testRepoFileExists: Repo file exists, list(syncany) returned one result.");
314                                return true;
315                        }
316                        else {
317                                logger.log(Level.INFO, "testRepoFileExists: Repo file DOES NOT exist.");
318                                return false;
319                        }
320                }
321                catch (Exception e) {
322                        logger.log(Level.INFO, "testRepoFileExists: Repo file DOES NOT exist. Exception occurred.", e);
323                        return false;
324                }
325        }
326
327        @Override
328        public boolean testTargetCanCreate() {
329                if (Files.isWritable(repoPath.getParent())) {
330                        logger.log(Level.INFO, "testTargetCanCreate: Can create target.");
331                        return true;
332                }
333                else {
334                        logger.log(Level.INFO, "testTargetCanCreate: Can NOT create target.");
335                        return false;
336                }
337        }
338}