/*
* Copyright 2004 - 2008 Christian Sprajc, Dennis Waldherr. 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.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Controller;
import de.dal33t.powerfolder.PFComponent;
import de.dal33t.powerfolder.disk.FolderStatistic;
import de.dal33t.powerfolder.light.FileInfo;
import de.dal33t.powerfolder.message.FileChunk;
import de.dal33t.powerfolder.transfer.Transfer.State;
import de.dal33t.powerfolder.transfer.Transfer.TransferState;
import de.dal33t.powerfolder.util.Base64;
import de.dal33t.powerfolder.util.Convert;
import de.dal33t.powerfolder.util.DateUtil;
import de.dal33t.powerfolder.util.Debug;
import de.dal33t.powerfolder.util.FileUtils;
import de.dal33t.powerfolder.util.Format;
import de.dal33t.powerfolder.util.ProgressListener;
import de.dal33t.powerfolder.util.Range;
import de.dal33t.powerfolder.util.Reject;
import de.dal33t.powerfolder.util.TransferCounter;
import de.dal33t.powerfolder.util.Util;
import de.dal33t.powerfolder.util.delta.FilePartsRecord;
import de.dal33t.powerfolder.util.delta.FilePartsState;
import de.dal33t.powerfolder.util.delta.FilePartsState.PartState;
import de.dal33t.powerfolder.util.delta.MatchCopyWorker;
import de.dal33t.powerfolder.util.delta.MatchInfo;
import de.dal33t.powerfolder.util.delta.MatchResultWorker;
import de.schlichtherle.truezip.file.TFile;
/**
* Shared implementation of download managers. This class leaves details on what
* to request from whom to the implementing class.
*
* @author Dennis "Bytekeeper" Waldherr
*/
public abstract class AbstractDownloadManager extends PFComponent implements
DownloadManager
{
private enum InternalState {
WAITING_FOR_SOURCE, WAITING_FOR_UPLOAD_READY, WAITING_FOR_FILEPARTSRECORD,
COMPLETED {
@Override
public boolean isDone() {
return true;
}
},
BROKEN {
@Override
public boolean isDone() {
return true;
}
},
ABORTED {
@Override
public boolean isDone() {
return true;
}
},
/**
* Request chunks
*/
ACTIVE_DOWNLOAD, MATCHING_AND_COPYING, CHECKING_FILE_VALIDITY, PASSIVE_DOWNLOAD;
public boolean isDone() {
return false;
}
}
protected FilePartsState filePartsState;
protected FilePartsRecord remotePartRecord;
private volatile TransferCounter counter;
private State transferState = new State();
private final FileInfo fileInfo;
private Controller controller;
private RandomAccessFile tempRAF = null;
/**
* Only set on init(boolean).
*/
private volatile InternalState state = null;
private volatile boolean automatic;
private volatile boolean started;
private volatile boolean shutdown;
private File metaFile;
private String fileID;
private File tempFile;
private final TransferManager tm;
private File metaDataBaseDir;
public AbstractDownloadManager(Controller controller, FileInfo file,
boolean automatic)
{
Reject.noNullElements(controller, file);
this.fileInfo = file;
this.automatic = automatic;
this.controller = controller;
tm = controller.getTransferManager();
}
public synchronized void abort() {
// illegalState("abort");
switch (state) {
case ABORTED :
illegalState("abort()");
break;
case BROKEN :
case COMPLETED :
break;
default :
setAborted(false);
break;
}
}
public synchronized void abortAndCleanup() {
// illegalState("abortAndCleanup");
switch (state) {
case ABORTED :
illegalState("abortAndCleanup()");
break;
case BROKEN :
case COMPLETED :
break;
default :
setAborted(true);
break;
}
}
public synchronized boolean addSource(Download download) {
validateDownload(download);
Reject.ifFalse(
download.isCompleted() || canAddSource(download.getPartner()),
"Illegal addSource() call!!");
return addSource0(download);
}
public synchronized void chunkReceived(Download download, FileChunk chunk) {
Reject.noNullElements(download, chunk);
validateDownload(download);
assert chunk.file.isVersionDateAndSizeIdentical(getFileInfo());
try {
receivedChunk0(download, chunk);
} catch (BrokenDownloadException e) {
setBroken(TransferProblem.BROKEN_DOWNLOAD, e.toString());
}
}
public synchronized void filePartsRecordReceived(Download download,
FilePartsRecord record)
{
Reject.noNullElements(download, record);
validateDownload(download);
try {
receivedFilePartsRecord0(download, record);
} catch (BrokenDownloadException e) {
setBroken(TransferProblem.BROKEN_DOWNLOAD, e.toString());
}
}
public Controller getController() {
return controller;
}
/**
* @return the transfer counter
*/
public synchronized TransferCounter getCounter() {
if (counter == null) {
counter = new TransferCounter(0, fileInfo.getSize());
}
return counter;
}
public FileInfo getFileInfo() {
return fileInfo;
}
public State getState() {
return transferState;
}
/**
* @return the tempfile for this download
*/
public synchronized File getTempFile() {
File diskFile = getFileInfo().getDiskFile(
getController().getFolderRepository());
if (diskFile == null) {
return null;
}
if (tempFile == null) {
try {
tempFile = new TFile(getMetaDataBaseDir(), "(incomplete) "
+ getFileID());
} catch (IOException e) {
logSevere("IOException", e);
return null;
}
}
return tempFile;
}
public boolean isBroken() {
return state == InternalState.BROKEN;
}
public boolean isCompleted() {
return state == InternalState.COMPLETED;
}
public boolean isDone() {
return state.isDone();
}
public boolean isRequestedAutomatic() {
return automatic;
}
public boolean isStarted() {
return started;
}
public synchronized void readyForRequests(Download download) {
validateDownload(download);
try {
readyForRequests0(download);
} catch (BrokenDownloadException e) {
setBroken(TransferProblem.BROKEN_DOWNLOAD, e.toString());
} catch (AssertionError e) {
logSevere("AssertionError", e);
throw e;
}
}
public synchronized void removeSource(Download download) {
validateDownload(download);
try {
removeSource0(download);
} catch (BrokenDownloadException e) {
setBroken(TransferProblem.BROKEN_DOWNLOAD, e.toString());
}
}
@Override
public String toString() {
String tInfo = tempFile == null ? "n/a" : tempFile.toString();
return "[" + getClass().getSimpleName() + "; state= " + state
+ " file=" + getFileInfo() + "; tempFileRAF: " + tempRAF
+ "; tempFile: " + tInfo + "; broken: " + isBroken()
+ "; completed: " + isCompleted() + "; aborted: " + isAborted()
+ "; partsState: " + filePartsState;
}
protected abstract void addSourceImpl(Download source);
protected boolean checkCompleted() {
setTransferState(TransferState.VERIFYING);
// logFine("Verifying file hash for " + this);
try {
FilePartsRecord thisRemotePartRecord = remotePartRecord;
byte[] tempFileHash = null;
if (thisRemotePartRecord != null) {
tempFileHash = FileUtils.digest(getTempFile(),
MessageDigest.getInstance("MD5"), new ProgressListener() {
public void progressReached(double percentageReached) {
setTransferState(percentageReached / 100.0);
}
});
}
// If we don't have a record, no hashing was performed and the file
// is assumed to be "valid"
if (tempFileHash == null
|| Arrays.equals(thisRemotePartRecord.getFileDigest(),
tempFileHash))
{
return true;
}
logWarning("Checksum test FAILED on " + fileInfo.toDetailString()
+ ". MD5 found: " + Base64.encodeBytes(tempFileHash)
+ " expected: "
+ Base64.encodeBytes(thisRemotePartRecord.getFileDigest()));
counter = new TransferCounter(0, fileInfo.getSize());
// filePartsState.setPartState(Range.getRangeByLength(0,
// filePartsState.getFileLength()), PartState.NEEDED);
filePartsState = null;
// Maybe part record was bogus.
remotePartRecord = null;
return false;
} catch (NoSuchAlgorithmException e) {
// If this error occurs, no downloads will ever succeed.
logSevere("NoSuchAlgorithmException", e);
throw new RuntimeException(e);
} catch (Exception e) {
logSevere("Exception", e);
setBroken(TransferProblem.GENERAL_EXCEPTION, e.getMessage());
}
return false;
}
protected File getFile() {
return fileInfo.getDiskFile(getController().getFolderRepository());
}
/**
* Call this after construction. Otherwise download might not have tempfile
* ready. Does not prepare tempfile if completed
*
* @param completed
* if this download is already completed.
* @throws IOException
*/
public synchronized void init(boolean completed) throws IOException {
assert fileInfo != null;
if (completed) {
setTransferState(TransferState.DONE, 1);
state = InternalState.COMPLETED;
} else {
setTransferState(TransferState.NONE);
state = InternalState.WAITING_FOR_SOURCE;
}
// If it's an old download, don't create a temporary file
if (isCompleted()) {
return;
}
if (getTempFile() == null) {
throw new IOException("Couldn't create a temporary file for "
+ fileInfo);
}
// This has to happen here since "completed" is valid
assert !isDone() : "File broken/aborted before init!";
assert getTempFile().getParentFile().exists() : "Missing PowerFolder system directory";
// Create temp-file directory structure if necessary
// if (!getTempFile().getParentFile().exists()) {
// if (!getTempFile().getParentFile().mkdirs()) {
// throw new FileNotFoundException(
// "Couldn't create parent directory!");
// }
// }
loadMetaData();
if (isFiner()) {
logFiner("Init tempfile at " + getTempFile());
}
tempRAF = new RandomAccessFile(getTempFile(), "rw");
}
protected boolean isNeedingFilePartsRecord() {
return !isCompleted() && remotePartRecord == null
&& fileInfo.getSize() >= Constants.MIN_SIZE_FOR_PARTTRANSFERS
&& fileInfo.diskFileExists(getController());
}
protected void matchAndCopyData() throws BrokenDownloadException,
InterruptedException
{
try {
File src = getFile();
setTransferState(TransferState.MATCHING);
ProgressListener transferObs = new ProgressListener() {
public void progressReached(double percentageReached) {
setTransferState(percentageReached);
}
};
Callable<List<MatchInfo>> mInfoWorker = new MatchResultWorker(
remotePartRecord, src, transferObs);
List<MatchInfo> mInfoRes = null;
mInfoRes = mInfoWorker.call();
// logFine("Records: " + record.getInfos().length);
if (isFine()) {
logFine("Matches: "
+ mInfoRes.size()
+ " which are "
+ Format.formatBytes(remotePartRecord.getPartLength()
* mInfoRes.size()) + " bytes (bit less maybe) on "
+ fileInfo.toDetailString());
}
setTransferState(TransferState.COPYING);
Callable<FilePartsState> pStateWorker = new MatchCopyWorker(src,
getTempFile(), remotePartRecord, mInfoRes, transferObs);
FilePartsState calcedState = pStateWorker.call();
if (calcedState.getFileLength() != fileInfo.getSize()) {
// Concurrent file modification
throw new BrokenDownloadException();
}
setFilePartsState(calcedState);
counter = new TransferCounter(filePartsState.countPartStates(
filePartsState.getRange(), PartState.AVAILABLE),
fileInfo.getSize());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA Digest not found. Fatal error", e);
} catch (FileNotFoundException e) {
throw new BrokenDownloadException(
TransferProblem.FILE_NOT_FOUND_EXCEPTION, e);
} catch (IOException e) {
throw new BrokenDownloadException(TransferProblem.IO_EXCEPTION, e);
} catch (InterruptedException e) {
throw e;
} catch (Exception e) {
throw new BrokenDownloadException(
TransferProblem.GENERAL_EXCEPTION, e);
}
}
protected abstract void removeSourceImpl(Download source);
protected abstract void requestFilePartsRecord(Download download);
/**
* Be careful with the implementation of this method, it's called with
* internal locks in place. Reason: This method will access filepartsstate,
* which is also accessed in here. TODO: Find a "cleaner" way so this method
* doesn't need to be locked.
*
* @throws BrokenDownloadException
*/
protected abstract void sendPartRequests() throws BrokenDownloadException;
protected synchronized void setAutomatic(boolean auto) {
automatic = auto;
}
public synchronized void setBroken(final TransferProblem problem,
final String message)
{
if (isBroken()) {
return;
}
if (isFine()) {
logFine("Download broken: " + fileInfo.toDetailString()
+ ". Problem: " + problem + ": " + message);
}
setState(InternalState.BROKEN);
shutdown();
if (problem.equals(TransferProblem.MD5_ERROR)) {
try {
deleteTempFile();
} catch (IOException e) {
logSevere("Unable to remove tempfile on MD5_ERROR: "
+ getTempFile() + ". " + e, e);
}
} else {
if (getTempFile() != null && getTempFile().exists()
&& getTempFile().length() == 0)
{
if (isFiner()) {
logFiner("Deleting tempfile with size 0.");
}
if (!getTempFile().delete()) {
logWarning("Failed to delete temp file: "
+ getTempFile().getAbsolutePath());
}
}
}
final Download sources[] = getSources().toArray(new Download[0]);
for (Download d : sources) {
d.setBroken(problem, message);
}
tm.downloadManagerBroken(AbstractDownloadManager.this, problem, message);
}
protected synchronized void setCompleted() {
assert !isCompleted();
// Might be broken/aborted in the mean time
if (isDone()) {
return;
}
if (isInfo()) {
if (fileInfo.getFolderInfo().isMetaFolder()) {
logFine("Download completed: " + fileInfo.toDetailString());
} else {
logInfo("Download completed: " + fileInfo.toDetailString());
}
}
setTransferState(TransferState.DONE, 1);
setState(InternalState.COMPLETED);
shutdown();
deleteMetaData();
tm.setCompleted(AbstractDownloadManager.this);
}
protected synchronized void setFilePartsState(FilePartsState state) {
assert filePartsState == null;
if (filePartsState != null) {
logSevere("FilePartsState should've been null, but was "
+ filePartsState + " on " + fileInfo.toDetailString());
Debug.dumpCurrentStackTrace();
}
filePartsState = state;
}
protected void setStarted() {
started = true;
}
protected void setTransferState(double progress) {
transferState.setProgress(progress);
for (Download d : getSources()) {
d.state.setProgress(progress);
}
}
protected void setTransferState(TransferState state) {
if (transferState.getState() == state) {
return;
}
transferState.setState(state);
transferState.setProgress(0);
for (Download d : getSources()) {
d.state.setState(state);
d.state.setProgress(0);
}
}
protected void setTransferState(TransferState state, double progress) {
transferState.setState(state);
transferState.setProgress(progress);
for (Download d : getSources()) {
d.state.setState(state);
d.state.setProgress(progress);
}
}
/**
* Releases resources not required anymore
*/
protected synchronized void shutdown() {
assert isDone();
if (shutdown) {
return;
}
assert tempRAF != null;
shutdown = true;
if (isFiner()) {
logFine("Shutting down " + fileInfo.toDetailString());
}
try {
if (isFiner()) {
logFiner("Closing temp file: " + getTempFile() + " of "
+ getFile());
}
tempRAF.close();
tempRAF = null;
if (isBroken()) {
saveMetaData();
} else {
deleteMetaData();
}
} catch (IOException e) {
logSevere("IOException", e);
}
// FIXME: Uncomment to save resources
// setFilePartsState(null);
// TODO: Actually the remote record shouldn't be dropped since if
// somebody wants to download the file from us
// we could just send it, instead of recalculating it!! (So it should be
// stored "somewhere" - like in the
// folders database or so)
remotePartRecord = null;
updateTempFile();
assert tempRAF == null;
assert !isCompleted() && !isAborted() || !hasMetaFile();
}
protected synchronized void startActiveDownload()
throws BrokenDownloadException
{
assert !getSources().isEmpty();
assert !isDone();
setStarted();
setState(InternalState.ACTIVE_DOWNLOAD);
if (isFiner()) {
logFiner("Requesting parts");
}
sendPartRequests();
}
protected synchronized void storeFileChunk(Download download,
FileChunk chunk)
{
assert download != null;
assert chunk != null;
assert getSources().contains(download) : "Invalid source!";
setStarted();
try {
tempRAF.seek(chunk.offset);
tempRAF.write(chunk.data);
} catch (IOException e) {
logSevere("IOException", e);
setBroken(TransferProblem.IO_EXCEPTION,
"Couldn't write to tempfile!");
return;
}
getCounter().chunkTransferred(chunk);
Range range = Range.getRangeByLength(chunk.offset, chunk.data.length);
filePartsState.setPartState(range, PartState.AVAILABLE);
long avs = filePartsState.countPartStates(filePartsState.getRange(),
PartState.AVAILABLE);
setTransferState(TransferState.DOWNLOADING, fileInfo.getSize() > 0
? (double) avs / fileInfo.getSize()
: 1);
// add bytes to transferred status
FolderStatistic stat = fileInfo.getFolder(
getController().getFolderRepository()).getStatistic();
if (stat != null) {
stat.getDownloadCounter().chunkTransferred(chunk);
}
}
/**
* @return true, if the download was actually requested from the source.
*/
private boolean addSource0(final Download download) {
// This should be true because the addSource() caller should be
// locking the calls
assert download.isCompleted() || canAddSource(download.getPartner()) : "Illegal addSource() call!!";
switch (state) {
case BROKEN :
// The connection could've broken while some other code
// tries to add sources
download.setBroken(TransferProblem.BROKEN_DOWNLOAD,
"Manager already broken!");
return false;
case MATCHING_AND_COPYING :
case CHECKING_FILE_VALIDITY :
case ACTIVE_DOWNLOAD :
case WAITING_FOR_UPLOAD_READY :
case WAITING_FOR_FILEPARTSRECORD :
addSourceImpl(download);
// Maybe rework the code so this request will be
// sent if we
// really need data
download.request(0);
return true;
case WAITING_FOR_SOURCE :
// Required for complete on load downloads
if (download.isCompleted()) {
setState(InternalState.COMPLETED);
}
addSourceImpl(download);
if (isDone()) {
return false;
}
// Zero sized files hack: Shouldn't request anything
if (getFileInfo().getSize() == 0) {
setCompleted();
return false;
}
long _offset = 0;
if (filePartsState != null) {
assert !filePartsState.isCompleted();
Range range = filePartsState
.findFirstPart(PartState.NEEDED);
if (range != null) {
_offset = range.getStart();
} else {
assert filePartsState.isCompleted()
|| filePartsState.findFirstPart(PartState.PENDING) != null;
}
}
setState(InternalState.WAITING_FOR_UPLOAD_READY);
download.request(_offset);
return true;
case COMPLETED :
addSourceImpl(download);
if (!download.isCompleted() && !download.isBroken()) {
download.setCompleted();
}
return false;
default :
illegalState("addSource");
return false;
}
}
private void validateDownload(Download download) {
Reject.ifNull(download, "Download is null!");
if (!download.getFile().isVersionDateAndSizeIdentical(getFileInfo())) {
throw new IllegalArgumentException("Download FileInfo differs: "
+ download.getFile().toDetailString() + " vs mine: "
+ getFileInfo().toDetailString());
}
}
private synchronized void checkFileValidity() {
assert state == InternalState.ACTIVE_DOWNLOAD
|| state == InternalState.WAITING_FOR_UPLOAD_READY
&& filePartsState.getFileLength() == 0
|| state == InternalState.MATCHING_AND_COPYING : "Invalid state: "
+ state;
setState(InternalState.CHECKING_FILE_VALIDITY);
tm.doWork(new Runnable() {
public void run() {
if (checkCompleted()) {
setCompleted();
} else {
setBroken(TransferProblem.MD5_ERROR, "File hash mismatch!");
}
}
});
}
private void deleteMetaData() {
if (getMetaFile() != null && !getMetaFile().delete()) {
if (isSevere() && getMetaFile().exists()) {
logSevere("Couldn't delete meta data file!");
}
}
}
/**
* @param id
* @return
* @throws Error
*/
private String getFileID() throws Error {
if (fileID == null) {
fileID = new String(Util.encodeHex(Util.md5(getFileInfo()
.getRelativeName().getBytes(Convert.UTF8))));
}
return fileID;
}
/**
* Returns the base directory for transfer specific meta data. If the
* directory doesn't exist, it's created.
*
* @return the base directory
* @throws IOException
* if the directory couldn't be created
*/
private File getMetaDataBaseDir() throws IOException {
if (metaDataBaseDir != null) {
return metaDataBaseDir;
}
// FIXME: Implement
boolean workAroundTrueZIP = getFileInfo().getFolder(
getController().getFolderRepository()).isEncrypted();
if (workAroundTrueZIP) {
metaDataBaseDir = new TFile(System.getProperty("tmp.dir"),
"transfers");
} else {
metaDataBaseDir = new TFile(getFileInfo().getFolder(
getController().getFolderRepository()).getSystemSubDir(),
"transfers");
}
if (!metaDataBaseDir.exists() && !metaDataBaseDir.mkdirs()) {
throw new IOException(
"Couldn't create base directory for transfer meta data!");
}
return metaDataBaseDir;
}
private boolean hasMetaFile() {
return getMetaFile() != null && getMetaFile().exists();
}
private File getMetaFile() {
if (metaFile != null) {
return metaFile;
}
File diskFile = getFileInfo().getDiskFile(
getController().getFolderRepository());
if (diskFile == null) {
return null;
}
try {
metaFile = new TFile(getMetaDataBaseDir(),
FileUtils.DOWNLOAD_META_FILE + getFileID());
return metaFile;
} catch (IOException e) {
logSevere("IOException", e);
return null;
}
}
private void illegalState(String operation) {
throw new IllegalStateException(operation + " not allowed in state "
+ state);
}
private boolean isAborted() {
return state == InternalState.ABORTED;
}
/**
* @throws FileNotFoundException
* @throws IOException
*/
private void deleteTempFile() throws IOException {
boolean exists = getTempFile() != null && getTempFile().exists();
if (exists && isFine()) {
logFine("killTempFile: " + getTempFile() + ", size: "
+ getTempFile().length());
}
if (exists && !getTempFile().delete()) {
if (isWarning()) {
logWarning("Couldn't delete old temporary file, some other process could be using it! Trying to set it's length to 0. for file: "
+ getFileInfo().toDetailString());
}
RandomAccessFile f = new RandomAccessFile(getTempFile(), "rw");
try {
f.setLength(0);
} finally {
f.close();
}
}
}
private void loadMetaData() throws IOException {
// logWarning("loadMetaData()");
if (getTempFile() == null
|| !getTempFile().exists()
|| !DateUtil.equalsFileDateCrossPlattform(fileInfo
.getModifiedDate().getTime(), getTempFile().lastModified()))
{
// If something's wrong with the tempfile, kill the meta data file
// if it exists
deleteMetaData();
deleteTempFile();
return;
}
File mf = getMetaFile();
if (mf == null || !mf.exists()) {
deleteTempFile();
return;
}
ObjectInputStream in = null;
try {
in = new ObjectInputStream(new FileInputStream(mf));
FileInfo fi = (FileInfo) in.readObject();
if (fi.isVersionDateAndSizeIdentical(fileInfo)) {
List<?> content = (List<?>) in.readObject();
for (Object o : content) {
if (o.getClass() == FilePartsState.class) {
setFilePartsState((FilePartsState) o);
} else if (o.getClass() == FilePartsRecord.class) {
remotePartRecord = (FilePartsRecord) o;
}
}
} else {
in.close();
deleteTempFile();
deleteMetaData();
}
} catch (Exception e) {
remotePartRecord = null;
filePartsState = null;
if (in != null) {
try {
in.close();
} catch (IOException ex) {
}
}
deleteMetaData();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
}
}
}
if (filePartsState != null) {
if (isInfo()) {
logInfo("Resuming download - already got "
+ filePartsState.countPartStates(filePartsState.getRange(),
PartState.AVAILABLE) + " of " + getFileInfo().getSize());
}
}
}
private void protocolStateError(final Download cause, String operation)
throws BrokenDownloadException
{
String msg = "PROTOCOL ERROR caused by " + cause + ": " + operation
+ " not allowed in state " + state;
msg += " use DS: " + Util.useDeltaSync(getController(), cause)
+ " use Swarm: "
+ Util.useSwarming(getController(), cause.getPartner());
logWarning(msg);
throw new BrokenDownloadException(msg);
}
private void readyForRequests0(final Download download)
throws BrokenDownloadException
{
switch (state) {
case CHECKING_FILE_VALIDITY :
case MATCHING_AND_COPYING :
// Do nothing, action will be taken after the worker is done
break;
case ACTIVE_DOWNLOAD :
sendPartRequests();
break;
case WAITING_FOR_FILEPARTSRECORD :
// Maybe request from different source
requestFilePartsRecord(download);
break;
case WAITING_FOR_UPLOAD_READY :
if (isNeedingFilePartsRecord()
&& Util.useDeltaSync(getController(), download))
{
setState(InternalState.WAITING_FOR_FILEPARTSRECORD);
requestFilePartsRecord(download);
} else {
if (filePartsState == null) {
setFilePartsState(new FilePartsState(fileInfo.getSize()));
}
if (filePartsState.isCompleted()) {
if (isFine()) {
logFine("Not requesting anything, seems to be a zero file: "
+ fileInfo);
}
checkFileValidity();
} else {
if (isFiner()) {
logFiner("Not requesting record for this download.");
}
startActiveDownload();
}
}
break;
case COMPLETED :
case BROKEN :
case ABORTED :
download.abort();
break;
default :
protocolStateError(download, "readyForRequests");
break;
}
}
private void receivedChunk0(final Download download, FileChunk chunk)
throws BrokenDownloadException
{
switch (state) {
case ABORTED :
case BROKEN :
if (isFine()) {
logFine("Aborted download of " + fileInfo
+ " received chunk from " + download);
}
download.abort();
break;
case ACTIVE_DOWNLOAD :
storeFileChunk(download, chunk);
if (filePartsState.isCompleted()) {
checkFileValidity();
} else {
sendPartRequests();
}
break;
case PASSIVE_DOWNLOAD :
storeFileChunk(download, chunk);
if (filePartsState.isCompleted()) {
setCompleted();
}
break;
case WAITING_FOR_UPLOAD_READY :
setState(InternalState.PASSIVE_DOWNLOAD);
setFilePartsState(new FilePartsState(fileInfo.getSize()));
filePartsState.setPartState(filePartsState.getRange(),
PartState.NEEDED);
receivedChunk0(download, chunk);
break;
default :
protocolStateError(download, "receivedChunk");
break;
}
}
private void receivedFilePartsRecord0(Download download,
final FilePartsRecord record) throws BrokenDownloadException
{
switch (state) {
case WAITING_FOR_FILEPARTSRECORD :
if (isFine()) {
logFine("Matching and copying..."
+ fileInfo.toDetailString());
}
setState(InternalState.MATCHING_AND_COPYING);
remotePartRecord = record;
tm.doWork(new Runnable() {
public void run() {
try {
matchAndCopyData();
if (isDone()) {
return;
}
if (filePartsState.isCompleted()) {
checkFileValidity();
} else {
// Protect empty check
synchronized (AbstractDownloadManager.this) {
if (getSources().isEmpty()) {
throw new BrokenDownloadException(
"Out of sources");
}
if (!isDone()) {
try {
startActiveDownload();
} catch (BrokenDownloadException e) {
setBroken(
TransferProblem.IO_EXCEPTION,
e.toString());
}
}
}
}
} catch (final BrokenDownloadException e) {
setBroken(TransferProblem.IO_EXCEPTION,
e.toString());
} catch (InterruptedException e) {
logFiner("InterruptedException", e);
}
}
});
break;
default :
protocolStateError(download, "receivedFilePartsRecord");
break;
}
}
private void removeSource0(final Download download)
throws BrokenDownloadException
{
switch (state) {
case WAITING_FOR_FILEPARTSRECORD :
removeSourceImpl(download);
requestFilePartsRecord(null);
break;
case WAITING_FOR_UPLOAD_READY :
removeSourceImpl(download);
if (!hasSources()) {
// If we're out of sources, wait for additional ones
// again
// Actually the TransferManager will break this
// transfer, but with
// the following code this manager could also be reused.
setState(InternalState.WAITING_FOR_SOURCE);
}
break;
case ACTIVE_DOWNLOAD :
removeSourceImpl(download);
if (hasSources()) {
sendPartRequests();
}
break;
case PASSIVE_DOWNLOAD :
removeSourceImpl(download);
if (!isDone()) {
setBroken(TransferProblem.BROKEN_DOWNLOAD, "Source lost.");
}
break;
case MATCHING_AND_COPYING :
case CHECKING_FILE_VALIDITY :
case BROKEN :
case ABORTED :
case COMPLETED : // If a remote side sends an abort this can
// happen
removeSourceImpl(download);
break;
default :
illegalState("removeSource");
break;
}
}
private void saveMetaData() throws IOException {
assert state != InternalState.COMPLETED;
// logWarning("saveMetaData()");
File mf = getMetaFile();
if (mf == null && !isCompleted()) {
deleteTempFile();
return;
}
ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream(mf)));
try {
out.writeObject(fileInfo);
List<Object> list = new LinkedList<Object>();
if (filePartsState != null) {
filePartsState.purgePending();
list.add(filePartsState);
}
if (remotePartRecord != null) {
list.add(remotePartRecord);
}
out.writeObject(list);
} finally {
out.close();
}
}
private void setAborted(boolean cleanup) {
assert !isAborted();
assert Thread.holdsLock(this);
// Might have been completed/broken
if (isDone()) {
return;
}
if (isFine()) {
logFine("Download aborted: " + fileInfo);
}
setState(InternalState.ABORTED);
shutdown();
if (cleanup) {
try {
deleteTempFile();
} catch (FileNotFoundException e) {
logSevere("FileNotFoundException", e);
} catch (IOException e) {
logSevere("IOException", e);
}
deleteMetaData();
}
// Prevent ConcurrentModificiation Exceptions
final Download sources[] = getSources().toArray(new Download[0]);
for (Download d : sources) {
d.abort();
}
tm.downloadManagerAborted(AbstractDownloadManager.this);
}
private void setState(InternalState newState) {
assert Thread.holdsLock(this);
if (newState == InternalState.WAITING_FOR_UPLOAD_READY) {
assert filePartsState == null || !filePartsState.isCompleted();
}
if (isFiner()) {
logFiner("State change to " + newState + ": "
+ getFileInfo().toDetailString());
}
state = newState;
}
private void updateTempFile() {
if (getTempFile() == null
|| !getTempFile().setLastModified(
getFileInfo().getModifiedDate().getTime()))
{
logWarning("Failed to update modification date! Detail:" + this);
}
}
}