/*
* Copyright 2004 - 2008 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder 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.
*
* PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder.transfer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.LinkedList;
import java.util.Queue;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Member;
import de.dal33t.powerfolder.disk.Folder;
import de.dal33t.powerfolder.light.FileInfo;
import de.dal33t.powerfolder.message.FileChunk;
import de.dal33t.powerfolder.message.FileChunkExt;
import de.dal33t.powerfolder.message.Message;
import de.dal33t.powerfolder.message.ReplyFilePartsRecord;
import de.dal33t.powerfolder.message.RequestDownload;
import de.dal33t.powerfolder.message.RequestFilePartsRecord;
import de.dal33t.powerfolder.message.RequestPart;
import de.dal33t.powerfolder.message.StartUpload;
import de.dal33t.powerfolder.message.StartUploadExt;
import de.dal33t.powerfolder.message.StopUpload;
import de.dal33t.powerfolder.net.ConnectionException;
import de.dal33t.powerfolder.util.Convert;
import de.dal33t.powerfolder.util.DateUtil;
import de.dal33t.powerfolder.util.ProgressListener;
import de.dal33t.powerfolder.util.Reject;
import de.dal33t.powerfolder.util.Util;
import de.dal33t.powerfolder.util.delta.FilePartsRecord;
import de.schlichtherle.truezip.file.TFileInputStream;
/**
* Simple class for a scheduled Upload
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
* @version $Revision: 1.13 $
*/
@SuppressWarnings("serial")
public class Upload extends Transfer {
private boolean aborted;
private transient Queue<Message> pendingRequests = new LinkedList<Message>();
protected transient RandomAccessFile raf;
protected transient TFileInputStream in;
private long inpos;
private String debugState;
/**
* Constructs a new uploads, package protected, can only be called by
* transfermanager
*
* @param manager
* @param member
* @param dl
*/
Upload(TransferManager manager, Member member, RequestDownload dl) {
super(manager, (FileInfo) ((dl == null) ? null : dl.file), member);
if (dl == null) {
throw new NullPointerException("Download request is null");
}
if (dl.file == null) {
throw new NullPointerException("File is null");
}
setStartOffset(dl.startOffset);
aborted = false;
debugState = "initialized";
}
private void enqueueMessage(Message m) {
try {
synchronized (pendingRequests) {
if (pendingRequests.size() >= getTransferManager()
.getMaxRequestsQueued() * 5)
{
throw new TransferException("Too many requests queued: "
+ pendingRequests.size() + ", maximum: "
+ getTransferManager().getMaxRequestsQueued() * 5);
}
pendingRequests.add(m);
pendingRequests.notifyAll();
}
} catch (TransferException e) {
logSevere("TransferException", e);
getTransferManager().uploadBroken(this,
TransferProblem.TRANSFER_EXCEPTION, e.getMessage());
}
}
public void enqueuePartRequest(RequestPart pr) {
Reject.ifNull(pr, "Message is null");
// If the download was aborted
if (aborted || !isStarted()) {
return;
}
// Requests for different files on the same transfer connection are not
// supported currently
if (!pr.getFile().isVersionDateAndSizeIdentical(getFile())
|| pr.getRange().getLength() <= 0)
{
logSevere("Received invalid part request!");
getTransferManager().uploadBroken(this,
TransferProblem.INVALID_PART);
return;
}
if (pr.getRange().getLength() > getTransferManager()
.getMaxFileChunkSize())
{
logWarning("Got request for a range bigger then my max filechunk size ("
+ pr.getRange()
+ "): "
+ pr.getRange().getLength()
+ " on "
+ getFile().toDetailString());
}
state.setProgress(pr.getProgress());
enqueueMessage(pr);
}
public void receivedFilePartsRecordRequest(RequestFilePartsRecord r) {
Reject.ifNull(r, "Record is null");
if (isFine()) {
logFine("Received request for a parts record: " + r);
}
// If the download was aborted
if (aborted || !isStarted()) {
return;
}
if (getFile().getSize() < Constants.MIN_SIZE_FOR_PARTTRANSFERS) {
logWarning("Remote side requested invalid PartsRecordRequest!");
getTransferManager().uploadBroken(this,
TransferProblem.GENERAL_EXCEPTION,
"Remote side requested invalid PartsRecordRequest!");
return;
}
enqueueMessage(r);
}
public void stopUploadRequest(StopUpload su) {
Reject.ifNull(su, "Message is null");
synchronized (pendingRequests) {
pendingRequests.clear();
pendingRequests.add(su);
pendingRequests.notifyAll();
}
}
/**
* Starts the upload in a own thread using the give transfer manager
*/
synchronized void start() {
if (isStarted()) {
logWarning("Upload already started. " + this);
return;
}
if (isAborted() || isBroken()) {
logWarning("Upload already broken/aborted. " + this);
return;
}
debugState = "Starting";
// Mark upload as started
setStarted();
Runnable uploadPerfomer = new Runnable() {
public void run() {
try {
if (isAborted() || isBroken()) {
throw new TransferException(
"Upload broken/aborted while starting. "
+ Upload.this);
}
debugState = "Opening file";
if (!getFile().getFolder(
getController().getFolderRepository()).isEncrypted())
{
try {
raf = new RandomAccessFile(getFile().getDiskFile(
getController().getFolderRepository()), "r");
} catch (FileNotFoundException e) {
throw new TransferException(e);
}
} else {
try {
in = new TFileInputStream(getFile().getDiskFile(
getController().getFolderRepository()));
inpos = 0;
} catch (FileNotFoundException e) {
throw new TransferException(e);
}
}
if (isAborted() || isBroken()) {
throw new TransferException(
"Upload broken/aborted while starting. "
+ Upload.this);
}
// If our partner supports requests, let him request. This
// is required for swarming to work.
if (isFiner()) {
logFiner("Both clients support partial transfers!");
}
debugState = "Sending StartUpload";
try {
if (getPartner().getProtocolVersion() >= 102) {
getPartner().sendMessage(
new StartUploadExt(getFile()));
} else {
getPartner()
.sendMessage(new StartUpload(getFile()));
}
} catch (ConnectionException e) {
throw new TransferException(e);
}
debugState = "Waiting for requests";
if (waitForRequests(Constants.UPLOAD_REQUEST_TIMEOUT)) {
if (isFiner()) {
logFiner("Checking for parts request.");
}
debugState = "Checking for FPR request.";
// Check if the first request is for a
// FilePartsRecord
if (checkForFilePartsRecordRequest()) {
debugState = "Waiting for remote matching";
state.setState(TransferState.REMOTEMATCHING);
logFiner("Waiting for initial part requests!");
waitForRequests(Constants.UPLOAD_REMOTEHASHING_PART_REQUEST_TIMEOUT);
}
debugState = "Starting to send parts";
if (isFine()) {
logFine("Started " + this);
}
long startTime = System.currentTimeMillis();
// FIXME: It shouldn't be possible to loop endlessly
// This fixme has to solved somewhere else partly
// since
// it's like:
// "How long do we allow to upload to some party" -
// which can't be decided here.
while (sendPart()) {
}
long took = System.currentTimeMillis() - startTime;
getTransferManager().logTransfer(false, took,
getFile(), getPartner());
}
closeIO();
if (!isBroken() && !aborted) {
getTransferManager().setCompleted(Upload.this);
}
} catch (TransferException e) {
closeIO();
// Loggable.logWarningStatic(Upload.class, "Upload broken: "
// + Upload.this, e);
getTransferManager().uploadBroken(Upload.this,
TransferProblem.TRANSFER_EXCEPTION, e.getMessage());
} finally {
closeIO();
debugState = "DONE";
}
}
public String toString() {
return "Upload " + getFile().toDetailString() + " to "
+ getPartner().getNick();
}
};
// Perfom upload in threadpool
getTransferManager().perfomUpload(uploadPerfomer);
}
private synchronized void closeIO() {
if (in != null) {
try {
in.close();
in = null;
} catch (IOException e) {
logSevere("IOException", e);
}
}
if (raf != null) {
try {
if (isFiner()) {
logFiner("Closing raf for "
+ getFile().toDetailString());
}
raf.close();
raf = null;
} catch (IOException e) {
logSevere("IOException", e);
}
}
}
protected boolean checkForFilePartsRecordRequest() throws TransferException
{
RequestFilePartsRecord r = null;
synchronized (pendingRequests) {
if (pendingRequests.isEmpty()) {
logWarning("Cancelled message too fast");
return false;
}
if (pendingRequests.peek() instanceof RequestFilePartsRecord) {
r = (RequestFilePartsRecord) pendingRequests.remove();
}
}
if (r == null) {
return false;
}
final FileInfo fi = r.getFile();
checkLastModificationDate(fi,
fi.getDiskFile(getController().getFolderRepository()));
FilePartsRecord fpr;
try {
state.setState(TransferState.FILEHASHING);
fpr = getTransferManager().getFileRecordManager().retrieveRecord(
fi, new ProgressListener() {
public void progressReached(double percentageReached) {
state.setProgress(percentageReached);
}
});
getPartner().sendMessagesAsynchron(
new ReplyFilePartsRecord(fi, fpr));
state.setState(TransferState.UPLOADING);
} catch (FileNotFoundException e) {
logSevere("FileNotFoundException", e);
getTransferManager().uploadBroken(Upload.this,
TransferProblem.FILE_NOT_FOUND_EXCEPTION, e.getMessage());
} catch (IOException e) {
logSevere("IOException", e);
getTransferManager().uploadBroken(Upload.this,
TransferProblem.IO_EXCEPTION, e.getMessage());
}
return true;
}
/**
* Sends one requested part.
*
* @return false if the upload should stop, true otherwise
* @throws TransferException
*/
private boolean sendPart() throws TransferException {
if (getPartner() == null) {
throw new NullPointerException("Upload member is null");
}
if (getFile() == null) {
throw new NullPointerException("Upload file is null");
}
if (isAborted() || isBroken()) {
return false;
}
state.setState(TransferState.UPLOADING);
RequestPart pr = null;
long waitTime = Constants.UPLOAD_REMOTEHASHING_PART_REQUEST_TIMEOUT;
synchronized (pendingRequests) {
while (pendingRequests.isEmpty() && !isBroken() && !isAborted()) {
try {
pendingRequests.wait(waitTime);
waitTime = Constants.UPLOAD_REQUEST_TIMEOUT;
} catch (InterruptedException e) {
logWarning("Interrupted on " + this + ". " + e);
logFiner(e);
throw new TransferException(e);
}
}
// If it's still empty we either got a StopUpload, or we got
// interrupted or it got aborted in which case we just drop out.
// Also the timeout could be the cause in which case this also is
// the end of the upload.
if (pendingRequests.isEmpty()) {
return false;
}
if (pendingRequests.peek() instanceof StopUpload) {
pendingRequests.remove();
return false;
}
pr = (RequestPart) pendingRequests.remove();
if (isAborted() || isBroken()) {
return false;
}
}
File f = pr.getFile()
.getDiskFile(getController().getFolderRepository());
try {
byte[] data = new byte[(int) pr.getRange().getLength()];
long startOffset = pr.getRange().getStart();
if (raf != null) {
raf.seek(startOffset);
} else if (in != null) {
long skip = startOffset - inpos;
if (skip >= 0) {
inpos += in.skip(skip);
} else {
try {
try {
in.close();
} catch (Exception e) {
logWarning(e.toString());
}
in = new TFileInputStream(getFile().getDiskFile(
getController().getFolderRepository()));
in.skip(startOffset);
inpos = startOffset;
} catch (FileNotFoundException e) {
throw new TransferException(e);
}
}
}
int pos = 0;
while (pos < data.length) {
int read;
int readLen = data.length - pos;
if (raf != null) {
read = raf.read(data, pos, readLen);
} else if (in != null) {
read = in.read(data, pos, readLen);
inpos += read;
} else {
throw new TransferException("I/O already closed");
}
if (read < 0) {
logWarning("Requested part exceeds filesize!");
throw new TransferException(
"Requested part exceeds filesize!");
}
pos += read;
}
FileChunk chunk;
if (getPartner().getProtocolVersion() >= 104) {
chunk = new FileChunkExt(pr.getFile(),
pr.getRange().getStart(), data);
} else {
chunk = new FileChunk(pr.getFile(), pr.getRange().getStart(),
data);
}
getPartner().sendMessage(chunk);
getCounter().chunkTransferred(chunk);
getTransferManager().getUploadCounter().chunkTransferred(chunk);
// FIXME: Below this check is done every 15 seconds - maybe restrict
// this test here too
checkLastModificationDate(pr.getFile(), f);
} catch (FileNotFoundException e) {
logSevere("FileNotFoundException", e);
throw new TransferException(e);
} catch (IOException e) {
logSevere("IOException", e);
throw new TransferException(e);
} catch (ConnectionException e) {
logWarning("Connectiopn problem while uploading. " + e.toString());
if (isFiner()) {
logFiner("ConnectionException", e);
}
throw new TransferException(e);
}
return true;
}
protected boolean waitForRequests(long requestTimeoutMS) {
if (isBroken() || aborted) {
return false;
}
synchronized (pendingRequests) {
if (!pendingRequests.isEmpty()) {
return true;
}
try {
pendingRequests.wait(requestTimeoutMS);
} catch (InterruptedException e) {
logFine("InterruptedException. " + e);
}
}
return !isBroken() && !aborted && !pendingRequests.isEmpty();
}
/**
* Aborts this dl if currently transferrings
*/
synchronized void abort() {
logFiner("Upload aborted: " + this);
aborted = true;
stopUploads();
}
/**
* Shuts down this upload if currently active
*/
void shutdown() {
super.shutdown();
// "Forget" all requests from the client
stopUploads();
closeIO();
}
private void stopUploads() {
synchronized (pendingRequests) {
pendingRequests.clear();
// Notify any remaining waiter
pendingRequests.notifyAll();
}
}
public boolean isAborted() {
return aborted;
}
/**
* @return if this upload is broken
*/
public boolean isBroken() {
if (super.isBroken()) {
return true;
}
if (!stillQueuedAtPartner()) {
logWarning("Upload broken because not enqued @ partner: queedAtPartner: "
+ stillQueuedAtPartner()
+ ", folder: "
+ getFile().getFolder(getController().getFolderRepository())
+ ", diskfile: "
+ getFile().getDiskFile(getController().getFolderRepository())
+ ", last contime: " + getPartner().getLastConnectTime());
}
File diskFile = getFile().getDiskFile(
getController().getFolderRepository());
if (diskFile == null || !diskFile.exists()) {
logWarning("Upload broken because diskfile is not available, folder: "
+ getFile().getFolder(getController().getFolderRepository())
+ ", diskfile: "
+ diskFile
+ ", last contime: "
+ getPartner().getLastConnectTime());
return true;
}
return !stillQueuedAtPartner();
}
/*
* General
*/
public int hashCode() {
int hash = 0;
if (getFile() != null) {
hash += getFile().hashCode();
}
if (getPartner() != null) {
hash += getPartner().hashCode();
}
return hash;
}
public boolean equals(Object o) {
if (o instanceof Upload) {
Upload other = (Upload) o;
return Util.equals(this.getFile(), other.getFile())
&& Util.equals(this.getPartner(), other.getPartner());
}
return false;
}
public String toString() {
String msg = "Upload: State: " + debugState + ", TransferState: "
+ state.getState() + " " + getFile().toDetailString() + " to '"
+ getPartner().getNick() + "'";
if (getPartner().isOnLAN()) {
msg += " (local-net)";
}
return msg;
}
private void checkLastModificationDate(FileInfo theFile, File f)
throws TransferException
{
assert theFile != null;
assert f != null;
boolean lastModificationDataMismatch = !DateUtil
.equalsFileDateCrossPlattform(f.lastModified(), theFile
.getModifiedDate().getTime());
if (lastModificationDataMismatch) {
Folder folder = theFile.getFolder(getController()
.getFolderRepository());
if (folder.scanAllowedNow()) {
folder.scanChangedFile(theFile);
}
// folder.recommendScanOnNextMaintenance();
throw new TransferException("Last modification date mismatch. '"
+ f.getAbsolutePath()
+ "': expected "
+ Convert.convertToGlobalPrecision(theFile.getModifiedDate()
.getTime()) + ", actual "
+ Convert.convertToGlobalPrecision(f.lastModified()));
}
}
}