/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.plugins.transfer;
import java.io.File;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.chunk.Transformer;
import org.syncany.config.Config;
import org.syncany.config.LocalEventBus;
import org.syncany.operations.daemon.messages.UpUploadFileInTransactionSyncExternalEvent;
import org.syncany.operations.daemon.messages.UpUploadFileSyncExternalEvent;
import org.syncany.plugins.transfer.files.RemoteFile;
import org.syncany.plugins.transfer.files.TempRemoteFile;
import org.syncany.plugins.transfer.files.TransactionRemoteFile;
import org.syncany.plugins.transfer.to.ActionTO;
import org.syncany.plugins.transfer.to.ActionTO.ActionStatus;
import org.syncany.plugins.transfer.to.ActionTO.ActionType;
import org.syncany.plugins.transfer.to.TransactionTO;
/**
* This class represents a transaction in a remote system. It will keep track of
* what files are to be added and ensures atomic operation.
*
* @author Pim Otte
*/
public class RemoteTransaction {
private static final Logger logger = Logger.getLogger(RemoteTransaction.class.getSimpleName());
private TransferManager transferManager;
private Config config;
private TransactionTO transactionTO;
private LocalEventBus eventBus;
public RemoteTransaction(Config config, TransferManager transferManager) {
this(config, transferManager, new TransactionTO(config.getMachineName()));
}
public RemoteTransaction(Config config, TransferManager transferManager, TransactionTO transactionTO) {
this.config = config;
this.transferManager = transferManager;
this.transactionTO = transactionTO;
this.eventBus = LocalEventBus.getInstance();
}
/**
* Returns whether the transaction is empty.
*/
public boolean isEmpty() {
return transactionTO.getActions().size() == 0;
}
/**
* Adds a file to this transaction. Generates a temporary file to store it.
*/
public void upload(File localFile, RemoteFile remoteFile) throws StorageException {
TempRemoteFile temporaryRemoteFile = new TempRemoteFile(remoteFile);
logger.log(Level.INFO, "- Adding file to TX for UPLOAD: " + localFile + " -> Temp. remote file: " + temporaryRemoteFile
+ ", final location: " + remoteFile);
ActionTO action = new ActionTO();
action.setType(ActionType.UPLOAD);
action.setLocalTempLocation(localFile);
action.setRemoteLocation(remoteFile);
action.setRemoteTempLocation(temporaryRemoteFile);
transactionTO.addAction(action);
}
/**
* Adds the deletion of a file to this transaction. Generates a temporary file
* to store it while the transaction is being finalized.
*/
public void delete(RemoteFile remoteFile) throws StorageException {
TempRemoteFile temporaryRemoteFile = new TempRemoteFile(remoteFile);
logger.log(Level.INFO, "- Adding file to TX for DELETE: " + remoteFile + "-> Temp. remote file: " + temporaryRemoteFile);
ActionTO action = new ActionTO();
action.setType(ActionType.DELETE);
action.setRemoteLocation(remoteFile);
action.setRemoteTempLocation(temporaryRemoteFile);
transactionTO.addAction(action);
}
/**
* Commits this transaction by performing the required upload and
* delete operations. The method first moves all files to the temporary
* remote location. If no errors occur, all files are moved to their
* final location.
*
* <p>The method first writes a {@link TransactionRemoteFile} containing
* all actions to be performed and then uploads this file. Then it uploads
* new files (added by {@link #upload(File, RemoteFile) upload()} and moves
* deleted files to a temporary location (deleted by {@link #delete(RemoteFile) delete()}.
*
* <p>If this was successful, the transaction file is deleted and the
* temporary files. After deleting the transaction file, the transaction
* is successfully committed.
*/
public void commit() throws StorageException {
logger.log(Level.INFO, "Starting TX.commit() ...");
if (isEmpty()) {
logger.log(Level.INFO, "- Empty transaction, not committing anything.");
return;
}
File localTransactionFile = writeLocalTransactionFile();
TransactionRemoteFile remoteTransactionFile = uploadTransactionFile(localTransactionFile);
commit(localTransactionFile, remoteTransactionFile);
}
/**
* Does exactly the same as the parameterless version, except it does not create and upload the transactionfile. Instead
* it uses the files that are passed. Used for resuming existing transactions. Only call this function if resuming
* cannot cause invalid states.
*/
public void commit(File localTransactionFile, TransactionRemoteFile remoteTransactionFile) throws StorageException {
logger.log(Level.INFO, "- Starting to upload data in commit.");
uploadAndMoveToTempLocation();
moveToFinalLocation();
deleteTransactionFile(localTransactionFile, remoteTransactionFile);
deleteTempRemoteFiles();
}
/**
* This method serializes the current state of the {@link RemoteTransaction} to a file.
*
* @param transactionFile The file where the transaction will be written to.
*/
public void writeToFile(Transformer transformer, File transactionFile) throws StorageException {
try {
transactionTO.save(transformer, transactionFile);
logger.log(Level.INFO, "Wrote transaction manifest to temporary file: " + transactionFile);
}
catch (Exception e) {
throw new StorageException("Could not write transaction to file: " + transactionFile, e);
}
}
/**
* This method serializes the transaction to a local temporary file.
*/
private File writeLocalTransactionFile() throws StorageException {
try {
File localTransactionFile = config.getCache().createTempFile("transaction");
writeToFile(config.getTransformer(), localTransactionFile);
return localTransactionFile;
}
catch (Exception e) {
throw new StorageException("Could not create temporary file for transaction", e);
}
}
/**
* This method uploads a local copy of the transaction to the repository. This is done at the begin of commit()
* and is the starting point of the transaction itself.
*/
private TransactionRemoteFile uploadTransactionFile(File localTransactionFile) throws StorageException {
TransactionRemoteFile remoteTransactionFile = new TransactionRemoteFile(this);
eventBus.post(new UpUploadFileSyncExternalEvent(config.getLocalDir().getAbsolutePath(), remoteTransactionFile.getName()));
logger.log(Level.INFO, "- Uploading remote transaction file {0} ...", remoteTransactionFile);
transferManager.upload(localTransactionFile, remoteTransactionFile);
return remoteTransactionFile;
}
/**
* This method performs the first step for all files in the committing process.
* For UPLOADs, this is uploading the file to the temporary remote location.
* For DELETEs, this is moving the file from the original remote location to a temporary remote location.
* If this is a transaction that is being resumed, the {@link ActionStatus} will show that this part has
* already been done. In this case, we do not repeat it.
*
* This is the expensive part of the committing process, when we are talking about I/O. Hence this is also
* the most likely part to be interrupted on weak connections.
*/
private void uploadAndMoveToTempLocation() throws StorageException {
TransactionStats stats = gatherTransactionStats();
int uploadFileIndex = 0;
for (ActionTO action : transactionTO.getActions()) {
if (action.getStatus().equals(ActionStatus.UNSTARTED)) {
// If we are resuming, this has not been started yet.
RemoteFile tempRemoteFile = action.getTempRemoteFile();
if (action.getType().equals(ActionType.UPLOAD)) {
// The action is an UPLOAD, upload file to temporary remote location
File localFile = action.getLocalTempLocation();
long localFileSize = localFile.length();
eventBus.post(new UpUploadFileInTransactionSyncExternalEvent(config.getLocalDir().getAbsolutePath(), ++uploadFileIndex,
stats.totalUploadFileCount, localFileSize, stats.totalUploadSize));
logger.log(Level.INFO, "- Uploading {0} to temp. file {1} ...", new Object[] { localFile, tempRemoteFile });
transferManager.upload(localFile, tempRemoteFile);
action.setStatus(ActionStatus.STARTED);
}
else if (action.getType().equals(ActionType.DELETE)) {
// The action is a DELETE, move file to temporary remote location.
RemoteFile remoteFile = action.getRemoteFile();
try {
logger.log(Level.INFO, "- Moving {0} to temp. file {1} ...", new Object[] { remoteFile, tempRemoteFile });
transferManager.move(remoteFile, tempRemoteFile);
}
catch (StorageMoveException e) {
logger.log(Level.INFO, " -> FAILED (don't care!), because the remoteFile does not exist: " + remoteFile);
}
action.setStatus(ActionStatus.STARTED);
}
}
}
}
/**
* This method gathers the total number of files and size that is to be uploaded.
*
* This is used in displays to the user.
*/
private TransactionStats gatherTransactionStats() {
TransactionStats stats = new TransactionStats();
for (ActionTO action : transactionTO.getActions()) {
if (action.getType().equals(ActionType.UPLOAD)) {
if (action.getStatus().equals(ActionStatus.UNSTARTED)) {
stats.totalUploadFileCount++;
stats.totalUploadSize += action.getLocalTempLocation().length();
}
}
}
return stats;
}
/**
* This method constitutes the second step in the committing process. All files have been uploaded, and they are
* now moved to their final location.
*/
private void moveToFinalLocation() throws StorageException {
for (ActionTO action : transactionTO.getActions()) {
if (action.getType().equals(ActionType.UPLOAD)) {
RemoteFile tempRemoteFile = action.getTempRemoteFile();
RemoteFile finalRemoteFile = action.getRemoteFile();
logger.log(Level.INFO, "- Moving temp. file {0} to final location {1} ...", new Object[] { tempRemoteFile, finalRemoteFile });
transferManager.move(tempRemoteFile, finalRemoteFile);
action.setStatus(ActionStatus.DONE);
}
}
}
/**
* This method deletes the transaction file. The deletion of the transaction file is the moment the transaction
* is considered to be finished and successful.
*/
private void deleteTransactionFile(File localTransactionFile, TransactionRemoteFile remoteTransactionFile) throws StorageException {
// After this deletion, the transaction is final!
logger.log(Level.INFO, "- Deleting remote transaction file {0} ...", remoteTransactionFile);
transferManager.delete(remoteTransactionFile);
localTransactionFile.delete();
logger.log(Level.INFO, "END of TX.commmit(): Succesfully committed transaction.");
}
/**
* This method deletes the temporary remote files that were the result of deleted files.
*
* Actually deleting remote files is done after finishing the transaction, because
* it cannot be rolled back! If this fails, the temporary files will eventually
* be cleaned up by Cleanup and download will not download these, because
* they are not in any transaction file.
*/
private void deleteTempRemoteFiles() throws StorageException {
boolean success = true;
for (ActionTO action : transactionTO.getActions()) {
if (action.getStatus().equals(ActionStatus.STARTED)) {
// If we are resuming, this action has not been comopleted.
if (action.getType().equals(ActionType.DELETE)) {
RemoteFile tempRemoteFile = action.getTempRemoteFile();
logger.log(Level.INFO, "- Deleting temp. file {0} ...", new Object[] { tempRemoteFile });
try {
transferManager.delete(tempRemoteFile);
}
catch (Exception e) {
logger.log(Level.INFO, "Failed to delete: " + tempRemoteFile, " because of: " + e);
success = false;
}
action.setStatus(ActionStatus.DONE);
}
}
}
if (success) {
logger.log(Level.INFO, "END of TX.delTemp(): Sucessfully deleted final files.");
}
else {
logger.log(Level.INFO, "END of TX.delTemp(): Did not succesfully delete all files!");
}
}
private static class TransactionStats {
private long totalUploadSize;
private int totalUploadFileCount;
}
}