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;
019
020import java.util.Arrays;
021import java.util.List;
022import java.util.Map;
023import java.util.logging.Level;
024import java.util.logging.Logger;
025
026import org.syncany.config.Config;
027import org.syncany.config.LocalEventBus;
028import org.syncany.plugins.transfer.StorageException;
029import org.syncany.plugins.transfer.TransferManager;
030import org.syncany.plugins.transfer.TransferManagerFactory;
031import org.syncany.plugins.transfer.features.ReadAfterWriteConsistent;
032import org.syncany.plugins.transfer.features.PathAware;
033import org.syncany.plugins.transfer.features.Retriable;
034import org.syncany.plugins.transfer.features.TransactionAware;
035import org.syncany.plugins.transfer.features.TransactionAwareFeatureTransferManager;
036import org.syncany.plugins.transfer.files.ActionRemoteFile;
037import org.syncany.plugins.transfer.files.CleanupRemoteFile;
038import org.syncany.plugins.transfer.files.DatabaseRemoteFile;
039
040/**
041 * Represents and is inherited by a transfer operation. Transfer operations are operations
042 * that modify the repository and/or are relevant for the consistency of the local directory
043 * or the remote repository.
044 *
045 * <p>This abstract class offers convenience methods to handle {@link ActionRemoteFile} as well
046 * as to handle the connection and local cache.
047 *
048 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
049 */
050public abstract class AbstractTransferOperation extends Operation {
051        private static final Logger logger = Logger.getLogger(AbstractTransferOperation.class.getSimpleName());
052
053        /**
054         * Defines the time after which old/outdated action files from other clients are
055         * deleted. This time must be significantly larger than the time action files are
056         * renewed by the {@link ActionFileHandler}.
057         *
058         * @see ActionFileHandler#ACTION_RENEWAL_INTERVAL
059         */
060        private static final int ACTION_FILE_DELETE_TIME = ActionFileHandler.ACTION_RENEWAL_INTERVAL + 5 * 60 * 1000; // Minutes
061
062        protected TransactionAwareFeatureTransferManager transferManager;
063        protected ActionFileHandler actionHandler;
064
065        protected LocalEventBus eventBus;
066
067        public AbstractTransferOperation(Config config, String operationName) {
068                super(config);
069
070                this.eventBus = LocalEventBus.getInstance();
071
072                try {
073                        // Do NOT reuse TransferManager for action file renewal; see #140
074
075                        TransferManager actionFileTransferManager = TransferManagerFactory
076                                        .build(config)
077                                        .withFeature(ReadAfterWriteConsistent.class)
078                                        .withFeature(Retriable.class)
079                                        .asDefault();
080
081                        TransactionAwareFeatureTransferManager regularFileTransferManager = TransferManagerFactory
082                                        .build(config)
083                                        .withFeature(ReadAfterWriteConsistent.class)
084                                        .withFeature(Retriable.class)
085                                        .withFeature(PathAware.class)
086                                        .withFeature(TransactionAware.class)
087                                        .as(TransactionAware.class);
088
089                        this.actionHandler = new ActionFileHandler(actionFileTransferManager, operationName, config.getMachineName());
090                        this.transferManager = regularFileTransferManager;
091                }
092                catch (StorageException e) {
093                        logger.log(Level.SEVERE, "Unable to create AbstractTransferOperation: Unable to create TransferManager", e);
094                        throw new RuntimeException("Unable to create AbstractTransferOperation: Unable to create TransferManager: " + e.getMessage());
095                }
096        }
097
098        protected void startOperation() throws Exception {
099                actionHandler.start();
100        }
101
102        protected void finishOperation() throws StorageException {
103                actionHandler.finish();
104
105                cleanActionFiles();
106                disconnectTransferManager();
107                clearCache();
108        }
109
110        protected boolean otherRemoteOperationsRunning(String... operationIdentifiers) throws StorageException {
111                logger.log(Level.INFO, "Looking for other running remote operations ...");
112                Map<String, ActionRemoteFile> actionRemoteFiles = transferManager.list(ActionRemoteFile.class);
113
114                boolean otherRemoteOperationsRunning = false;
115                List<String> disallowedOperationIdentifiers = Arrays.asList(operationIdentifiers);
116
117                for (ActionRemoteFile actionRemoteFile : actionRemoteFiles.values()) {
118                        String operationName = actionRemoteFile.getOperationName();
119                        String machineName = actionRemoteFile.getClientName();
120
121                        boolean isOwnActionFile = machineName.equals(config.getMachineName());
122                        boolean isOperationAllowed = !disallowedOperationIdentifiers.contains(operationName);
123                        boolean isOutdatedActionFile = isOutdatedActionFile(actionRemoteFile);
124
125                        if (!isOwnActionFile) {
126                                if (!isOutdatedActionFile) {
127                                        if (isOperationAllowed) {
128                                                logger.log(Level.INFO, "- Action file from other client, but allowed operation; not marking running; " + actionRemoteFile);
129                                        }
130                                        else {
131                                                logger.log(Level.INFO, "- Action file from other client; --> marking operations running (!); " + actionRemoteFile);
132                                                otherRemoteOperationsRunning = true;
133                                        }
134                                }
135                                else {
136                                        logger.log(Level.INFO, "- Action file outdated; ignoring " + actionRemoteFile);
137                                }
138                        }
139                }
140
141                return otherRemoteOperationsRunning;
142        }
143
144        private void cleanActionFiles() throws StorageException {
145                logger.log(Level.INFO, "Cleaning own old action files ...");
146                Map<String, ActionRemoteFile> actionRemoteFiles = transferManager.list(ActionRemoteFile.class);
147
148                for (ActionRemoteFile actionRemoteFile : actionRemoteFiles.values()) {
149                        String machineName = actionRemoteFile.getClientName();
150
151                        boolean isOwnActionFile = machineName.equals(config.getMachineName());
152                        boolean isOutdatedActionFile = isOutdatedActionFile(actionRemoteFile);
153
154                        if (isOwnActionFile) {
155                                logger.log(Level.INFO, "- Deleting own action file " + actionRemoteFile + " ...");
156                                transferManager.delete(actionRemoteFile);
157                        }
158                        else if (isOutdatedActionFile) {
159                                logger.log(Level.INFO, "- Action file from other client is OUTDATED; deleting " + actionRemoteFile + " ...");
160                                transferManager.delete(actionRemoteFile);
161                        }
162                        else {
163                                logger.log(Level.INFO, "- Action file is current; ignoring " + actionRemoteFile + " ...");
164                        }
165                }
166        }
167
168        /**
169         * This method is used to determine how a database file should be named when
170         * it is about to be uploaded. It returns the number of the newest database file (which is the
171         * highest number).
172         *
173         * @param client name of the client for which we want to upload a database version.
174         * @param knownDatabases all DatabaseRemoteFiles present in the repository
175         * @return the largest database fileversion number.
176         */
177        protected long getNewestDatabaseFileVersion(String client, List<DatabaseRemoteFile> knownDatabases) {
178                // TODO [low] This could be done via the "known_databases" database table
179
180                // Obtain last known database file version number and increment it
181                long clientVersion = 0;
182
183                for (DatabaseRemoteFile databaseRemoteFile : knownDatabases) {
184                        if (databaseRemoteFile.getClientName().equals(client)) {
185                                clientVersion = Math.max(clientVersion, databaseRemoteFile.getClientVersion());
186                        }
187                }
188
189                return clientVersion;
190        }
191
192        protected long getLastRemoteCleanupNumber(Map<String, CleanupRemoteFile> cleanupFiles) {
193                long cleanupNumber = 0;
194
195                // Find the number of the last cleanup
196                for (CleanupRemoteFile cleanupRemoteFile : cleanupFiles.values()) {
197                        cleanupNumber = Math.max(cleanupNumber, cleanupRemoteFile.getCleanupNumber());
198                }
199
200                return cleanupNumber;
201        }
202
203        private boolean isOutdatedActionFile(ActionRemoteFile actionFile) {
204                // TODO [low] Even though this is UTC and the times frames are large, this might be an issue with different timezones or wrong system clocks
205                return System.currentTimeMillis() - ACTION_FILE_DELETE_TIME > actionFile.getTimestamp();
206        }
207
208        private void disconnectTransferManager() {
209                try {
210                        transferManager.disconnect();
211                }
212                catch (StorageException e) {
213                        logger.log(Level.FINE, "Could not disconnect the transfermanager", e);
214                }
215        }
216
217        private void clearCache() {
218                config.getCache().clear();
219        }
220}