/*
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.InetAddress;
import java.util.regex.*;
import util.StringUtils;
import util.xml.*;
import com.leafdigital.irc.api.*;
import com.leafdigital.ui.api.*;
import leafchat.core.api.*;
/**
* Window that lets user choose whether or not to accept a file.
*/
@UIHandler({"acceptexists", "accept"})
public class FileAcceptWindow
{
/**
* Label: Nickname.
*/
public Label nickTextUI;
/**
* Label: Filename.
*/
public Label fileTextUI;
/**
* Label: Resume information.
*/
public Label resumeOfferUI;
/**
* Label: File exists.
*/
public Label existingTextUI;
/**
* Button: Resume.
*/
public Button resumeUI;
/**
* Button: Rename.
*/
public Button renameUI;
private Server s;
private String nick;
private byte[] fileBytes;
private int port;
private long size;
private InetAddress address;
private File target,targetPartial;
private long resumePos;
private PluginContext context;
/**
* @param context Plugin context
* @param s Server
* @param user User who sent request
* @param address User address
* @param port User port
* @param fileBytes Name of file (in raw bytes)
* @param size Size of file
* @param encodingReference Used to determine preferred character encoding
*/
public FileAcceptWindow(PluginContext context,Server s,
IRCUserAddress user,InetAddress address,int port,byte[] fileBytes,long size,
IRCMsg encodingReference)
{
this.context=context;
this.s=s;
this.nick=user.getNick();
this.fileBytes=fileBytes;
this.address=address;
this.port=port;
this.size=size;
// Sanitise filename
String file=encodingReference.convertEncoding(fileBytes);
file=file.replaceAll("^.*[/\\\\]",""); // Get rid of any folder names
StringBuffer clean=new StringBuffer();
for(int i=0;i<file.length();i++)
{
char c=file.charAt(i);
if(Character.isLetterOrDigit(c) || "._-,".indexOf(c)!=-1)
clean.append(c);
else
clean.append('_');
}
file=clean.toString();
// Get file target location
File folder=((DCCPlugin)context.getPlugin()).getDownloadFolder();
target=new File(folder,file);
targetPartial=new File(folder,file+".partial");
if(targetPartial.exists() && targetPartial.length()==0)
targetPartial.delete();
if(target.exists() && target.length()==0)
target.delete();
UI u=context.getSingle(UI.class);
// Does the file exist already?
if(target.exists() || targetPartial.exists())
{
w=u.createWindow("acceptexists", this);
existingTextUI.setText(existingTextUI.getText().replaceAll("%EXISTINGSIZE%",
StringUtils.displayBytes(targetPartial.exists() ? targetPartial.length() : target.length())));
// If the offered file is bigger than current...
if(targetPartial.exists() && (size==TransferProgress.SIZE_UNKNOWN || size>targetPartial.length()))
{
// May be OK to resume
}
else
{
resumeOfferUI.setVisible(false);
resumeUI.setVisible(false);
renameUI.setDefault(true);
}
}
else
{
w=u.createWindow("accept", this);
}
nickTextUI.setText(nickTextUI.getText().
replaceAll("%NICK%",XML.esc(user.getNick())).
replaceAll("%IP%",address.getHostAddress()));
String fileText=fileTextUI.getText();
fileText=fileText.replaceAll("%FILENAME%",XML.esc(file));
fileText=fileText.replaceAll("%SIZE%",
size==TransferProgress.SIZE_UNKNOWN
? "unknown size"
: StringUtils.displayBytes(size));
fileTextUI.setText(fileText);
w.setTitle(user.getNick()+" - file offer");
w.show(true);
}
private void sendResume(long pos)
{
try
{
ByteArrayOutputStream baos=new ByteArrayOutputStream();
baos.write(IRCMsg.constructBytes(
"PRIVMSG "+nick+" :\u0001DCC RESUME "));
baos.write(fileBytes);
baos.write(IRCMsg.constructBytes(" "+port+" "+pos+"\u0001"));
s.sendLine(baos.toByteArray());
context.logDebug("Sent DCC RESUME: "+IRCMsg.convertISO(baos.toByteArray()));
}
catch(IOException e)
{
throw new Error("Unexpected error sending resume request",e);
}
}
/**
* Message: DCC Accept.
* @param msg Message
*/
public void msg(UserCTCPRequestIRCMsg msg)
{
// Check it's a DCC ACCEPT for this user/file/etc
if(msg.getServer()!=s || !msg.getSourceUser().getNick().equals(nick) ||
!msg.getRequest().equals("DCC"))
return;
byte[][] params=IRCMsg.splitBytes(msg.getText());
if(params.length<4) return;
if(!IRCMsg.convertISO(params[0]).equals("ACCEPT")) return;
// Ignore parameter 1, some clients don't give the filename
if(!IRCMsg.convertISO(params[2]).equals(port+"")) return;
// OK, we got it!
try
{
resumePos=Long.parseLong(IRCMsg.convertISO(params[3]));
}
catch(NumberFormatException nfe)
{
return;
}
msg.markHandled();
context.logDebug("Received DCC ACCEPT: "+msg.getLineISO());
target.delete(); // Just in case
w.close();
startDownload();
}
private Window w;
/**
* Callback: Window closed.
*/
@UIAction
public void windowClosed()
{
context.unrequestMessages(null,this,PluginContext.ALLREQUESTS);
}
private void startDownload()
{
((DCCPlugin)context.getPlugin()).startDownload(nick,address,port,target,targetPartial,size,resumePos);
}
/**
* Action: Resume button.
*/
@UIAction
public void actionResume()
{
sendResume(targetPartial.length());
}
/**
* Action: Overwrite button.
*/
@UIAction
public void actionOverwrite()
{
targetPartial.delete();
target.delete();
w.close();
startDownload();
}
private final static Pattern RENAMEPATTERN=Pattern.compile("^(.*_)([0-9]+)$");
/**
* Action: Rename button.
*/
@UIAction
public void actionRename()
{
// Pick a new filename using _2, _3 etc
while(target.exists() || targetPartial.exists())
{
String name=target.getName();
File folder=target.getParentFile();
int dot=name.lastIndexOf('.');
String extension= (dot==-1) ? "" : name.substring(dot);
String main= (dot==-1) ? name : name.substring(0,dot);
Matcher m=RENAMEPATTERN.matcher(main);
if(m.matches())
{
main=m.group(1)+(Integer.parseInt(m.group(2))+1);
}
else
{
main+="_2";
}
target=new File(folder,main+extension);
targetPartial=new File(folder,main+extension+".partial");
}
targetPartial.delete();
w.close();
startDownload();
}
/**
* Action: Ignore button. (Ignores this request, does not actually /ignore
* the user.)
*/
@UIAction
public void actionIgnore()
{
w.close();
}
}