/* This file is part of leafdigital leafChat. leafChat 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. leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2011 Samuel Marshall. */ package com.leafdigital.dcc; import java.io.*; import java.net.*; import com.leafdigital.net.api.Network; import leafchat.core.api.*; /** * Thread that handles DCC downloads. */ public class Downloader extends Thread { private TransferProgress tp; private InetAddress address; private int port; private long pos,startPos; private long size; private File target,targetPartial; private PluginContext context; private FileOutputStream output; /** * @param context Plugin context * @param tp Receives progress information * @param address Address for connection * @param port Port for connection * @param target Final target file location * @param targetPartial Location to store partial download file * @param resumePos Position to resume from * @param size Download size */ public Downloader(PluginContext context,TransferProgress tp,InetAddress address,int port,File target,File targetPartial,long resumePos,long size) { // Set up values we'll need in thread this.context=context; this.tp=tp; this.address=address; this.port=port; this.pos=resumePos; this.startPos=resumePos; this.target=target; this.targetPartial=targetPartial; this.size=size; // Get file ready try { if(resumePos==0) output=new FileOutputStream(targetPartial); else { if(!targetPartial.exists()) throw new BugException("Unexpected resume position - partial file doesn't exist"); if(resumePos>targetPartial.length()) throw new BugException("Unexpected resume position - doesn't match file length"); if(resumePos<targetPartial.length()) { RandomAccessFile raf=new RandomAccessFile(targetPartial,"wb"); raf.setLength(resumePos); raf.close(); } output=new FileOutputStream(targetPartial,true); } } catch(IOException ioe) { tp.error("Couldn't open local file"); return; } // Check whether address looks plausible if(address.getHostAddress().startsWith("127.")) { tp.error("Request gave localhost address"); return; } if(port<1024 || port>65535) { tp.error("Request gave invalid port"); return; } tp.setDownloader(this); // Start thread start(); } void cancel() { cancelled=true; } private final static int BUFFERSIZE=65536; private boolean cancelled; @Override public void run() { // Connect tp.status("Connecting..."); InputStream is; OutputStream os; Socket s; Network n=context.getSingle(Network.class); try { context.log("Beginning DCC transfer "+target.getName()+" from "+address+":"+port); s=n.connect(address.getHostAddress(),port,30000); s.setSoTimeout(1000); is=s.getInputStream(); os=s.getOutputStream(); } catch(IOException e) { context.log("DCC failed: connection failed",e); tp.error("Connection failed"); return; } // Set initial position tp.status("Receiving..."); tp.setTransferred(pos); // Read data try { byte[] buffer=new byte[BUFFERSIZE]; long sentConfirmAt=0; long maxBlock=0; int confirmCount=0; boolean lastGotNothing=false; while(!cancelled) { int read; try { // Read data read=is.read(buffer); if(read==-1) { context.logDebug("Read EOF"); break; } else { context.logDebug("Read "+read+" bytes"); } lastGotNothing=false; } catch(SocketTimeoutException e) { if(cancelled) return; // We know what biggest block size is so don't send confirms more often, // probably just network delay if(pos >= sentConfirmAt+maxBlock || (pos>sentConfirmAt && lastGotNothing)) { try { sendPos(os); } catch(IOException e1) { context.log("DCC failed: error confirming data",e); tp.error("Error confirming data",e1); return; } sentConfirmAt=pos; confirmCount++; } lastGotNothing=true; continue; } catch(IOException e) { context.log("DCC failed: error reading data",e); tp.error("Error reading data"); return; } try { // Save in file output.write(buffer,0,read); } catch(IOException e) { context.log("DCC failed: error saving data",e); tp.error("Error saving data"); return; } // Update position pos+=read; tp.setTransferred(pos); // Track largest observed block size (between confirmations) maxBlock=Math.max(read,maxBlock); // Send confirmation immediately if they seem to need it, and // every 4096 bytes regardless if((confirmCount>5 && maxBlock<4096) || (pos-sentConfirmAt>=4096)) { try { sendPos(os); } catch(IOException e1) { context.log("DCC failed: error confirming data",e1); tp.error("Error confirming data",e1); return; } sentConfirmAt=pos; } } if(!cancelled) { if(pos>sentConfirmAt) { try { sendPos(os); } catch(IOException ioe) { // Who cares, we're done now } } tp.setFinished(); if(size==TransferProgress.SIZE_UNKNOWN || size==pos) { targetPartial.renameTo(target); } } } finally { try { context.log("DCC complete"); s.close(); output.close(); } catch(IOException e) { } } } private void sendPos(OutputStream os) throws IOException { // mIRC, BitchX may require the ack to be the // number of bytes sent this session rather than overall long sendPos=pos-startPos; int b1=(int)((sendPos>>24)&0xff); int b2=(int)((sendPos>>16)&0xff); int b3=(int)((sendPos>>8)&0xff); int b4=(int)(sendPos&0xff); os.write(b1); os.write(b2); os.write(b3); os.write(b4); context.logDebug("Sending ack: "+sendPos); os.flush(); } }