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.transfer;
019
020import java.io.File;
021import java.util.logging.Level;
022import java.util.logging.Logger;
023
024import org.syncany.chunk.Transformer;
025import org.syncany.config.Config;
026import org.syncany.config.LocalEventBus;
027import org.syncany.operations.daemon.messages.UpUploadFileInTransactionSyncExternalEvent;
028import org.syncany.operations.daemon.messages.UpUploadFileSyncExternalEvent;
029import org.syncany.plugins.transfer.files.RemoteFile;
030import org.syncany.plugins.transfer.files.TempRemoteFile;
031import org.syncany.plugins.transfer.files.TransactionRemoteFile;
032import org.syncany.plugins.transfer.to.ActionTO;
033import org.syncany.plugins.transfer.to.ActionTO.ActionStatus;
034import org.syncany.plugins.transfer.to.ActionTO.ActionType;
035import org.syncany.plugins.transfer.to.TransactionTO;
036
037/**
038 * This class represents a transaction in a remote system. It will keep track of
039 * what files are to be added and ensures atomic operation.
040 *
041 * @author Pim Otte
042 */
043public class RemoteTransaction {
044        private static final Logger logger = Logger.getLogger(RemoteTransaction.class.getSimpleName());
045
046        private TransferManager transferManager;
047        private Config config;
048        private TransactionTO transactionTO;
049
050        private LocalEventBus eventBus;
051
052        public RemoteTransaction(Config config, TransferManager transferManager) {
053                this(config, transferManager, new TransactionTO(config.getMachineName()));
054        }
055
056        public RemoteTransaction(Config config, TransferManager transferManager, TransactionTO transactionTO) {
057                this.config = config;
058                this.transferManager = transferManager;
059                this.transactionTO = transactionTO;
060                this.eventBus = LocalEventBus.getInstance();
061        }
062
063        /**
064         * Returns whether the transaction is empty.
065         */
066        public boolean isEmpty() {
067                return transactionTO.getActions().size() == 0;
068        }
069
070        /**
071         * Adds a file to this transaction. Generates a temporary file to store it.
072         */
073        public void upload(File localFile, RemoteFile remoteFile) throws StorageException {
074                TempRemoteFile temporaryRemoteFile = new TempRemoteFile(remoteFile);
075
076                logger.log(Level.INFO, "- Adding file to TX for UPLOAD: " + localFile + " -> Temp. remote file: " + temporaryRemoteFile
077                                + ", final location: " + remoteFile);
078
079                ActionTO action = new ActionTO();
080                action.setType(ActionType.UPLOAD);
081                action.setLocalTempLocation(localFile);
082                action.setRemoteLocation(remoteFile);
083                action.setRemoteTempLocation(temporaryRemoteFile);
084
085                transactionTO.addAction(action);
086        }
087
088        /**
089         * Adds the deletion of a file to this transaction. Generates a temporary file
090         * to store it while the transaction is being finalized.
091         */
092        public void delete(RemoteFile remoteFile) throws StorageException {
093                TempRemoteFile temporaryRemoteFile = new TempRemoteFile(remoteFile);
094
095                logger.log(Level.INFO, "- Adding file to TX for DELETE: " + remoteFile + "-> Temp. remote file: " + temporaryRemoteFile);
096
097                ActionTO action = new ActionTO();
098                action.setType(ActionType.DELETE);
099                action.setRemoteLocation(remoteFile);
100                action.setRemoteTempLocation(temporaryRemoteFile);
101
102                transactionTO.addAction(action);
103        }
104
105        /**
106         * Commits this transaction by performing the required upload and
107         * delete operations. The method first moves all files to the temporary
108         * remote location. If no errors occur, all files are moved to their
109         * final location.
110         *
111         * <p>The method first writes a {@link TransactionRemoteFile} containing
112         * all actions to be performed and then uploads this file. Then it uploads
113         * new files (added by {@link #upload(File, RemoteFile) upload()} and moves
114         * deleted files to a temporary location (deleted by {@link #delete(RemoteFile) delete()}.
115         *
116         * <p>If this was successful, the transaction file is deleted and the
117         * temporary files. After deleting the transaction file, the transaction
118         * is successfully committed.
119         */
120        public void commit() throws StorageException {
121                logger.log(Level.INFO, "Starting TX.commit() ...");
122
123                if (isEmpty()) {
124                        logger.log(Level.INFO, "- Empty transaction, not committing anything.");
125                        return;
126                }
127
128                File localTransactionFile = writeLocalTransactionFile();
129                TransactionRemoteFile remoteTransactionFile = uploadTransactionFile(localTransactionFile);
130
131                commit(localTransactionFile, remoteTransactionFile);
132        }
133
134        /**
135         * Does exactly the same as the parameterless version, except it does not create and upload the transactionfile. Instead
136         * it uses the files that are passed. Used for resuming existing transactions. Only call this function if resuming
137         * cannot cause invalid states.
138         */
139        public void commit(File localTransactionFile, TransactionRemoteFile remoteTransactionFile) throws StorageException {
140                logger.log(Level.INFO, "- Starting to upload data in commit.");
141
142                uploadAndMoveToTempLocation();
143                moveToFinalLocation();
144
145                deleteTransactionFile(localTransactionFile, remoteTransactionFile);
146                deleteTempRemoteFiles();
147        }
148
149        /**
150         * This method serializes the current state of the {@link RemoteTransaction} to a file.
151         * 
152         * @param transactionFile The file where the transaction will be written to.
153         */
154        public void writeToFile(Transformer transformer, File transactionFile) throws StorageException {
155                try {
156                        transactionTO.save(transformer, transactionFile);
157                        logger.log(Level.INFO, "Wrote transaction manifest to temporary file: " + transactionFile);
158                }
159                catch (Exception e) {
160                        throw new StorageException("Could not write transaction to file: " + transactionFile, e);
161                }
162        }
163
164        /**
165         * This method serializes the transaction to a local temporary file.
166         */
167        private File writeLocalTransactionFile() throws StorageException {
168                try {
169                        File localTransactionFile = config.getCache().createTempFile("transaction");
170                        writeToFile(config.getTransformer(), localTransactionFile);
171
172                        return localTransactionFile;
173                }
174                catch (Exception e) {
175                        throw new StorageException("Could not create temporary file for transaction", e);
176                }
177        }
178
179        /**
180         * This method uploads a local copy of the transaction to the repository. This is done at the begin of commit()
181         * and is the starting point of the transaction itself.
182         */
183        private TransactionRemoteFile uploadTransactionFile(File localTransactionFile) throws StorageException {
184                TransactionRemoteFile remoteTransactionFile = new TransactionRemoteFile(this);
185
186                eventBus.post(new UpUploadFileSyncExternalEvent(config.getLocalDir().getAbsolutePath(), remoteTransactionFile.getName()));
187
188                logger.log(Level.INFO, "- Uploading remote transaction file {0} ...", remoteTransactionFile);
189                transferManager.upload(localTransactionFile, remoteTransactionFile);
190
191                return remoteTransactionFile;
192        }
193
194        /**
195         * This method performs the first step for all files in the committing process. 
196         * For UPLOADs, this is uploading the file to the temporary remote location.
197         * For DELETEs, this is moving the file from the original remote location to a temporary remote location.
198         * If this is a transaction that is being resumed, the {@link ActionStatus} will show that this part has
199         * already been done. In this case, we do not repeat it.
200         * 
201         * This is the expensive part of the committing process, when we are talking about I/O. Hence this is also
202         * the most likely part to be interrupted on weak connections.
203         */
204        private void uploadAndMoveToTempLocation() throws StorageException {
205                TransactionStats stats = gatherTransactionStats();
206                int uploadFileIndex = 0;
207
208                for (ActionTO action : transactionTO.getActions()) {
209                        if (action.getStatus().equals(ActionStatus.UNSTARTED)) {
210                                // If we are resuming, this has not been started yet.
211                                RemoteFile tempRemoteFile = action.getTempRemoteFile();
212
213                                if (action.getType().equals(ActionType.UPLOAD)) {
214                                        // The action is an UPLOAD, upload file to temporary remote location
215                                        File localFile = action.getLocalTempLocation();
216                                        long localFileSize = localFile.length();
217
218                                        eventBus.post(new UpUploadFileInTransactionSyncExternalEvent(config.getLocalDir().getAbsolutePath(), ++uploadFileIndex,
219                                                        stats.totalUploadFileCount, localFileSize, stats.totalUploadSize));
220
221                                        logger.log(Level.INFO, "- Uploading {0} to temp. file {1} ...", new Object[] { localFile, tempRemoteFile });
222                                        transferManager.upload(localFile, tempRemoteFile);
223                                        action.setStatus(ActionStatus.STARTED);
224                                }
225                                else if (action.getType().equals(ActionType.DELETE)) {
226                                        // The action is a DELETE, move file to temporary remote location.
227                                        RemoteFile remoteFile = action.getRemoteFile();
228
229                                        try {
230                                                logger.log(Level.INFO, "- Moving {0} to temp. file {1} ...", new Object[] { remoteFile, tempRemoteFile });
231                                                transferManager.move(remoteFile, tempRemoteFile);
232                                        }
233                                        catch (StorageMoveException e) {
234                                                logger.log(Level.INFO, "  -> FAILED (don't care!), because the remoteFile does not exist: " + remoteFile);
235                                        }
236                                        action.setStatus(ActionStatus.STARTED);
237                                }
238                        }
239                }
240        }
241
242        /**
243         * This method gathers the total number of files and size that is to be uploaded.
244         * 
245         * This is used in displays to the user.
246         */
247        private TransactionStats gatherTransactionStats() {
248                TransactionStats stats = new TransactionStats();
249
250                for (ActionTO action : transactionTO.getActions()) {
251                        if (action.getType().equals(ActionType.UPLOAD)) {
252                                if (action.getStatus().equals(ActionStatus.UNSTARTED)) {
253                                        stats.totalUploadFileCount++;
254                                        stats.totalUploadSize += action.getLocalTempLocation().length();
255                                }
256                        }
257                }
258
259                return stats;
260        }
261
262        /**
263         * This method constitutes the second step in the committing process. All files have been uploaded, and they are
264         * now moved to their final location.
265         */
266        private void moveToFinalLocation() throws StorageException {
267                for (ActionTO action : transactionTO.getActions()) {
268                        if (action.getType().equals(ActionType.UPLOAD)) {
269                                RemoteFile tempRemoteFile = action.getTempRemoteFile();
270                                RemoteFile finalRemoteFile = action.getRemoteFile();
271
272                                logger.log(Level.INFO, "- Moving temp. file {0} to final location {1} ...", new Object[] { tempRemoteFile, finalRemoteFile });
273                                transferManager.move(tempRemoteFile, finalRemoteFile);
274                                action.setStatus(ActionStatus.DONE);
275                        }
276                }
277        }
278
279        /**
280         * This method deletes the transaction file. The deletion of the transaction file is the moment the transaction
281         * is considered to be finished and successful.
282         */
283        private void deleteTransactionFile(File localTransactionFile, TransactionRemoteFile remoteTransactionFile) throws StorageException {
284                // After this deletion, the transaction is final!
285                logger.log(Level.INFO, "- Deleting remote transaction file {0} ...", remoteTransactionFile);
286
287                transferManager.delete(remoteTransactionFile);
288                localTransactionFile.delete();
289
290                logger.log(Level.INFO, "END of TX.commmit(): Succesfully committed transaction.");
291        }
292
293        /**
294         * This method deletes the temporary remote files that were the result of deleted files.
295         * 
296         * Actually deleting remote files is done after finishing the transaction, because
297         * it cannot be rolled back! If this fails, the temporary files will eventually
298         * be cleaned up by Cleanup and download will not download these, because
299         * they are not in any transaction file.
300         */
301        private void deleteTempRemoteFiles() throws StorageException {
302                boolean success = true;
303                for (ActionTO action : transactionTO.getActions()) {
304                        if (action.getStatus().equals(ActionStatus.STARTED)) {
305                                // If we are resuming, this action has not been comopleted.
306                                if (action.getType().equals(ActionType.DELETE)) {
307                                        RemoteFile tempRemoteFile = action.getTempRemoteFile();
308
309                                        logger.log(Level.INFO, "- Deleting temp. file {0}  ...", new Object[] { tempRemoteFile });
310                                        try {
311                                                transferManager.delete(tempRemoteFile);
312                                        }
313                                        catch (Exception e) {
314                                                logger.log(Level.INFO, "Failed to delete: " + tempRemoteFile, " because of: " + e);
315                                                success = false;
316                                        }
317                                        action.setStatus(ActionStatus.DONE);
318                                }
319                        }
320                }
321
322                if (success) {
323                        logger.log(Level.INFO, "END of TX.delTemp(): Sucessfully deleted final files.");
324                }
325                else {
326                        logger.log(Level.INFO, "END of TX.delTemp(): Did not succesfully delete all files!");
327                }
328        }
329
330        private static class TransactionStats {
331                private long totalUploadSize;
332                private int totalUploadFileCount;
333        }
334}