//
// $Id: FileUploadThreadFTP.java 136 2007-05-12 20:15:36 +0000 (sam., 12 mai
// 2007) felfert $
//
// jupload - A file upload applet.
// Copyright 2007 The JUpload Team
//
// Created: 2007-01-01
// Creator: etienne_sf
// Last modified: $Date: 2010-02-26 07:45:47 -0300 (Sex, 26 Fev 2010) $
//
// 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 2 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, write to the Free Software Foundation, Inc.,
// 675 Mass Ave, Cambridge, MA 02139, USA.
package wjhk.jupload2.upload;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.concurrent.BlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPConnectionClosedException;
import org.apache.commons.net.ftp.FTPReply;
import wjhk.jupload2.exception.JUploadException;
import wjhk.jupload2.exception.JUploadExceptionUploadFailed;
import wjhk.jupload2.exception.JUploadIOException;
import wjhk.jupload2.policies.UploadPolicy;
/**
* The FileUploadThreadFTP class is intended to extend the functionality of the
* JUpload applet and allow it to handle ftp:// addresses.
* Note: this class is not a V4 of the FTP upload. It is named V4, as it
* inherits from the {@link FileUploadThread} class.
*
* In order to use it, simply change the postURL argument to the applet to
* contain the appropriate ftp:// link. The format is:
*
*
* ftp://username:password@myhost.com:21/directory
*
*
* Where everything but the host is optional. There is another parameter that
* can be passed to the applet named 'binary' which will set the file transfer
* mode based on the value. The possible values here are 'true' or 'false'. It
* was intended to be somewhat intelligent by looking at the file extension and
* basing the transfer mode on that, however, it was never implemented. Feel
* free to! Also, there is a 'passive' parameter which also has a value of
* 'true' or 'false' which sets the connection type to either active or passive
* mode.
*
* @author Evin Callahan (inheritance from DefaultUploadThread built by
* etienne_sf)
* @author Daystar Computer Services
* @see FileUploadThread
* @see DefaultFileUploadThread
* @version 1.0, 01 Jan 2007 * Update march 2007, etienne_sf Adaptation to match
* all JUpload functions: Inheritance from the
* {@link FileUploadThread} class, Use of the UploadFileData class,
* Before upload file preparation, Upload stop by the user.
*
*/
public class FileUploadThreadFTP extends DefaultFileUploadThread {
// ////////////////////////////////////////////////////////////////////////////////////
// /////////////////////// PRIVATE ATTRIBUTES
// ///////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////
// /////////////////////// PRIVATE ATTRIBUTES
// ///////////////////////////////////////
// ////////////////////////////////////////////////////////////////////////////////////
/**
* The output stream, where the current file should be written. This output
* stream should not be used. The buffered one is much faster.
*/
private OutputStream ftpOutputStream = null;
/**
* The buffered stream, that the application should use for upload.
*/
private BufferedOutputStream bufferedOutputStream = null;
private Matcher uriMatch;
// the client that does the actual connecting to the server
private FTPClient ftp = new FTPClient();
/** FTP user, taken from the postURL applet parameter */
private String user;
/** FTP password, taken from the postURL applet parameter */
private String pass;
/** FTP target host, taken from the postURL applet parameter */
private String host;
/** FTP target port, taken from the postURL applet parameter */
private String port;
/**
* FTP target root folder for the upload, taken from the postURL applet
* parameter
*/
private String ftpRootFolder;
/**
* Indicates whether the connection to the FTP server is open or not. This
* allows to connect once on the FTP server, for multiple file upload.
*/
private boolean bConnected = false;
/**
* This pattern defines the groups and pattern of the ftp syntax.
*/
public final Pattern ftpPattern = Pattern
.compile("^ftp://(([^:]+):([^\\@]+)\\@)?([^/:]+):?([0-9]+)?(/(.*))?$");
/**
* Creates a new instance. Performs the connection to the server based on
* the matcher created in the main.
*
* @param uploadPolicy
* @param packetQueue The queue from wich packets to upload are available.
* @param fileUploadManagerThread
* @throws JUploadException
* @throws IllegalArgumentException if any error occurs. message is error
*/
public FileUploadThreadFTP(UploadPolicy uploadPolicy,
BlockingQueue packetQueue,
FileUploadManagerThread fileUploadManagerThread)
throws JUploadException {
super("FileUploadThreadFTP thread", packetQueue, uploadPolicy,
fileUploadManagerThread);
this.uploadPolicy.displayDebug("[FileUploadThreadFTP] Using "
+ this.getClass().getName(), 30);
// Some coherence checks, for parameter given to the applet.
// stringUploadSuccess: unused in FTP mode. Must be null.
if (uploadPolicy.getStringUploadSuccess() != null) {
uploadPolicy
.displayWarn("FTP mode: stringUploadSuccess parameter ignored (forced to null)");
uploadPolicy.setProperty(UploadPolicy.PROP_STRING_UPLOAD_SUCCESS,
null);
}
// nbFilesPerRequest: must be 1 in FTP mode.
if (uploadPolicy.getNbFilesPerRequest() != 1) {
uploadPolicy
.displayWarn("FTP mode: nbFilesPerRequest parameter ignored (forced to 1)");
uploadPolicy.setProperty(UploadPolicy.PROP_NB_FILES_PER_REQUEST,
"1");
}
// maxChunkSize: must be unlimited (no chunk management in FTP mode).
if (uploadPolicy.getMaxChunkSize() != Long.MAX_VALUE) {
uploadPolicy
.displayWarn("FTP mode: maxChunkSize parameter ignored (forced to Long.MAX_VALUE)");
uploadPolicy.setProperty(UploadPolicy.PROP_MAX_CHUNK_SIZE, Long
.toString(Long.MAX_VALUE));
}
}
/** @see DefaultFileUploadThread#beforeRequest(UploadFilePacket) */
@Override
void beforeRequest(UploadFilePacket packet) throws JUploadException {
// If we're connected, we need to check the connection.
if (this.bConnected) {
// Let's check the connection is still Ok.
try {
this.ftp.sendNoOp();
} catch (FTPConnectionClosedException eClosed) {
// Let's forget this connection.
this.bConnected = false;
} catch (IOException e) {
throw new JUploadIOException(e.getClass().getName()
+ " while checking FTP connection to the server", e);
}
}
// If not already connected ... we connect to the server.
if (!this.bConnected) {
// Let's connect to the FTP server.
String url = this.uploadPolicy.getPostURL();
this.uriMatch = this.ftpPattern.matcher(url);
if (!this.uriMatch.matches()) {
throw new JUploadException("invalid URI: " + url);
}
this.user = this.uriMatch.group(2) == null ? "anonymous"
: this.uriMatch.group(2);
this.pass = this.uriMatch.group(3) == null ? "JUpload"
: this.uriMatch.group(3);
this.host = this.uriMatch.group(4); // no default server
this.port = this.uriMatch.group(5) == null ? "21" : this.uriMatch
.group(5);
this.ftpRootFolder = (this.uriMatch.group(7) == null) ? null : "/"
+ this.uriMatch.group(7);
// The last character must be a slash
if (this.ftpRootFolder != null && !this.ftpRootFolder.endsWith("/")) {
this.ftpRootFolder += "/";
}
// do connect.. any error will be thrown up the chain
try {
this.ftp.setDefaultPort(Integer.parseInt(this.port));
this.ftp.connect(this.host);
this.uploadPolicy.displayDebug(
"[FileUploadThreadFTP] Connected to " + this.host, 10);
this.uploadPolicy.displayDebug(this.ftp.getReplyString(), 80);
if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode()))
throw new JUploadException("FTP server refused connection.");
// given the login information, do the login
this.ftp.login(this.user, this.pass);
this.uploadPolicy.displayDebug("[FileUploadThreadFTP] "
+ this.ftp.getReplyString(), 80);
if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode()))
throw new JUploadException(
"Invalid ftp username / password");
this.bConnected = true;
} catch (JUploadException jue) {
// No special action, we keep the exception untouched
throw jue;
} catch (IOException ioe) {
throw new JUploadIOException(ioe.getClass().getName()
+ "[FTP] Could not connect to server ("
+ ioe.getMessage() + ")", ioe);
} catch (Exception e) {
throw new JUploadException(e.getClass().getName()
+ "[FTP] Could not connect to server ("
+ e.getMessage() + ")", e);
}
// now do the same for the passive/active parameter
if (this.uploadPolicy.getFtpTransfertPassive()) {
this.ftp.enterLocalPassiveMode();
} else {
this.ftp.enterLocalActiveMode();
}
} // if(!bConnected)
}
/** @see DefaultFileUploadThread#afterFile(UploadFileData) */
@Override
void afterFile(UploadFileData uploadFileData) {
// Nothing to do
}
/** @see DefaultFileUploadThread#beforeFile(UploadFilePacket, UploadFileData) */
@Override
void beforeFile(UploadFilePacket uploadFilePacket,
UploadFileData uploadFileData) throws JUploadException {
String workingDir = null;
try {
// if configured to, we go to the relative sub-folder of the current
// file, or on the root of the postURL.
if (this.uploadPolicy.getFtpCreateDirectoryStructure()) {
// We create the FTP directory structure
// TODO: call it once for all files, not once for each file.
createDirectoryStructure(uploadFilePacket);
workingDir = this.ftpRootFolder
+ uploadFileData.getRelativeDir();
// We want to have only slashes, as anti-slashes may generate
// FTP errors.
workingDir = workingDir.replace("\\", "/");
this.uploadPolicy
.displayDebug(
"[FileUploadThreadFTP] ftpCreateDirectoryStructure: Changing working directory to: "
+ workingDir, 80);
} else {
workingDir = this.ftpRootFolder;
}
if (workingDir != null && !workingDir.equals("")
&& !workingDir.equals(".")) {
this.ftp.changeWorkingDirectory(workingDir);
this.uploadPolicy.displayDebug("[FileUploadThreadFTP] "
+ this.ftp.getReplyString(), 80);
}
if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode())) {
throw new JUploadException(
"[FTP] Error while changing directory to: "
+ workingDir + " (" + this.ftp.getReplyString()
+ ")");
}
setTransferType(uploadFileData);
// No delete, as the user may not have the right for that. We use,
// later, the store command:
// If the file already exists, it will be replaced.
// ftp.deleteFile(filesToUpload[index].getFileName());
// Let's open the stream for this file.
this.ftpOutputStream = this.ftp.storeFileStream(uploadFileData
.getFileName());
if (this.ftpOutputStream == null) {
throw new JUploadIOException(
"Stream connection to the server error. Check that your path on the URL is valid. postURL used is: "
+ this.uploadPolicy.getPostURL());
}
// The upload is done through a BufferedOutputStream. This speed up
// the upload in an unbelievable way ...
this.bufferedOutputStream = new BufferedOutputStream(
this.ftpOutputStream);
} catch (IOException e) {
throw new JUploadIOException(e);
}
}
/** @see DefaultFileUploadThread#cleanAll() */
@Override
void cleanAll() {
try {
if (this.ftp.isConnected()) {
this.ftp.disconnect();
this.uploadPolicy.displayDebug(
"[FileUploadThreadFTP] disconnected", 50);
}
} catch (IOException e) {
// then we arent connected
this.uploadPolicy.displayDebug(
"[FileUploadThreadFTP] Not connected", 50);
} finally {
this.ftpOutputStream = null;
this.bufferedOutputStream = null;
}
}
/** @see DefaultFileUploadThread#cleanRequest() */
@Override
void cleanRequest() throws JUploadException {
if (this.bufferedOutputStream != null) {
try {
this.bufferedOutputStream.close();
this.ftpOutputStream.close();
if (!this.ftp.completePendingCommand()) {
throw new JUploadExceptionUploadFailed(
"ftp.completePendingCommand() returned false");
}
} catch (IOException e) {
throw new JUploadException(e);
} finally {
this.bufferedOutputStream = null;
}
}
}
/**
* @throws JUploadIOException
* @see DefaultFileUploadThread#finishRequest()
*/
@Override
int finishRequest() throws JUploadException {
try {
getOutputStream().flush();
return 200;
} catch (IOException ioe) {
throw new JUploadIOException("FileUploadThreadFTP.finishRequest()",
ioe);
} catch (Exception e) {
// When the user may not override an existing file, I got a
// NullPointerException. Let's trap all errors here.
throw new JUploadException(
"FileUploadThreadFTP.finishRequest() (check the user permission on the server)",
e);
}
}
/** @see DefaultFileUploadThread#getAdditionnalBytesForUpload(UploadFileData) */
@Override
long getAdditionnalBytesForUpload(UploadFileData uploadFileData) {
// Default: no additional byte.
return 0;
}
/** @see DefaultFileUploadThread#getOutputStream() */
@Override
OutputStream getOutputStream() {
return this.bufferedOutputStream;
}
/** @see DefaultFileUploadThread#startRequest(long, boolean, int, boolean) */
@Override
void startRequest(long contentLength, boolean bChunkEnabled, int chunkPart,
boolean bLastChunk) {
// Nothing to do
}
/**
* Will set the binary/ascii value based on the parameters to the applet.
* This could be done by file extension too but it is not implemented.
*
* @param uploadFileData The file that we want to upload.
* @throws IOException if an error occurs while setting mode data
*/
private void setTransferType(UploadFileData uploadFileData)
throws JUploadIOException {
try {
// read the value given from the user
if (this.uploadPolicy.getFtpTransfertBinary()) {
this.ftp.setFileType(FTP.BINARY_FILE_TYPE);
} else {
this.ftp.setFileType(FTP.ASCII_FILE_TYPE);
}
} catch (IOException ioe) {
throw new JUploadIOException(
"Cannot set transfert binary or ascii mode (binary: "
+ this.uploadPolicy.getFtpTransfertBinary() + ")",
ioe);
}
}
/**
* Create all relative sub-directories, so the structure on the server
* reflects the structure of the uploaded files.
*
* @throws JUploadIOException When an error occurs during folder creation
*/
// A tester
private void createDirectoryStructure(UploadFilePacket packet)
throws JUploadIOException {
SortedSet foldersToCreate = new TreeSet();
String folderName;
String intermediateFolderName;
StringTokenizer st;
// 1) Let's find all folders and sub-folders we'll have to create.
for (UploadFileData uploadFileData : packet) {
if (isInterrupted()) {
break;
}
folderName = uploadFileData.getRelativeDir();
folderName = folderName.replaceAll("\\\\", "/");
// Do we already have this folder ?
if (!foldersToCreate.contains(folderName)) {
// We add this folder, and all missing intermediate ones
st = new StringTokenizer(folderName, "/");
intermediateFolderName = this.ftpRootFolder;
while (st.hasMoreTokens()) {
intermediateFolderName += st.nextToken() + "/";
if (!foldersToCreate.contains(intermediateFolderName)) {
this.uploadPolicy.displayDebug(
"FTP structure identification: Adding subfolder "
+ intermediateFolderName, 80);
foldersToCreate.add(intermediateFolderName);
}
}
}
}
// 2) Let's create theses folders.
try {
String folder;
for (Iterator it = foldersToCreate.iterator(); it.hasNext();) {
folder = it.next();
// The folder is in the list of folder to create, created from
// the file list.
// We first check if the folder already exist.
this.ftp.changeWorkingDirectory(folder);
if (FTPReply.isPositiveCompletion(this.ftp.getReplyCode())) {
this.uploadPolicy.displayDebug(
"[FileUploadThreadFTP] Folder " + folder
+ " already exist", 80);
} else {
// We can not guess if it's because the folder
// doesn't exist, or if it's a 'real' error.
// Let's try to create the folder.
this.ftp.mkd(folder);
this.uploadPolicy.displayDebug(
"[FileUploadThreadFTP] Folder " + folder
+ " created", 80);
if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode())) {
throw new JUploadIOException(
"Error while creating folder '"
+ folder
+ "' ("
+ this.ftp.getReplyString().replaceAll(
"\r\n", "") + ")");
}
}
}
} catch (IOException ioe) {
throw new JUploadIOException(ioe.getClass().getName()
+ " in FileUploadThreadFTP.createDirectoryStructure()", ioe);
}
}
/** {@inheritDoc} */
void interruptionReceived() {
cleanAll();
}
}