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}