View Javadoc
1   //
2   // $Id: FileUploadThreadFTP.java 136 2007-05-12 20:15:36 +0000 (sam., 12 mai
3   // 2007) felfert $
4   //
5   // jupload - A file upload applet.
6   // Copyright 2007 The JUpload Team
7   //
8   // Created: 2007-01-01
9   // Creator: etienne_sf
10  // Last modified: $Date: 2014-05-01 19:03:23 +0200 (jeu., 01 mai 2014) $
11  //
12  // This program is free software; you can redistribute it and/or modify it under
13  // the terms of the GNU General Public License as published by the Free Software
14  // Foundation; either version 2 of the License, or (at your option) any later
15  // version. This program is distributed in the hope that it will be useful, but
16  // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17  // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
18  // details. You should have received a copy of the GNU General Public License
19  // along with this program; if not, write to the Free Software Foundation, Inc.,
20  // 675 Mass Ave, Cambridge, MA 02139, USA.
21  
22  package wjhk.jupload2.upload;
23  
24  import java.io.BufferedOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStream;
27  import java.util.Iterator;
28  import java.util.SortedSet;
29  import java.util.StringTokenizer;
30  import java.util.TreeSet;
31  import java.util.concurrent.BlockingQueue;
32  import java.util.regex.Matcher;
33  import java.util.regex.Pattern;
34  
35  import org.apache.commons.net.ftp.FTP;
36  import org.apache.commons.net.ftp.FTPClient;
37  import org.apache.commons.net.ftp.FTPConnectionClosedException;
38  import org.apache.commons.net.ftp.FTPReply;
39  
40  import wjhk.jupload2.exception.JUploadException;
41  import wjhk.jupload2.exception.JUploadExceptionUploadFailed;
42  import wjhk.jupload2.exception.JUploadIOException;
43  import wjhk.jupload2.policies.UploadPolicy;
44  
45  /**
46   * The FileUploadThreadFTP class is intended to extend the functionality of the
47   * JUpload applet and allow it to handle ftp:// addresses. <br>
48   * Note: this class is not a V4 of the FTP upload. It is named V4, as it
49   * inherits from the {@link FileUploadThread} class. <br>
50   * <br>
51   * In order to use it, simply change the postURL argument to the applet to
52   * contain the appropriate ftp:// link. The format is:
53   * 
54   * <pre>
55   *         ftp://username:password@myhost.com:21/directory
56   * </pre>
57   * 
58   * Where everything but the host is optional. There is another parameter that
59   * can be passed to the applet named 'binary' which will set the file transfer
60   * mode based on the value. The possible values here are 'true' or 'false'. It
61   * was intended to be somewhat intelligent by looking at the file extension and
62   * basing the transfer mode on that, however, it was never implemented. Feel
63   * free to! Also, there is a 'passive' parameter which also has a value of
64   * 'true' or 'false' which sets the connection type to either active or passive
65   * mode.
66   * 
67   * @author Evin Callahan (inheritance from DefaultUploadThread built by
68   *         etienne_sf)
69   * @author Daystar Computer Services
70   * @see FileUploadThread
71   * @see DefaultFileUploadThread
72   * @version 1.0, 01 Jan 2007 * Update march 2007, etienne_sf Adaptation to match
73   *          all JUpload functions: <DIR> <LI>Inheritance from the
74   *          {@link FileUploadThread} class, <LI>Use of the UploadFileData class,
75   *          <LI>Before upload file preparation, <LI>Upload stop by the user. <LI>
76   *          </DIR>
77   */
78  public class FileUploadThreadFTP extends DefaultFileUploadThread {
79  
80      // ////////////////////////////////////////////////////////////////////////////////////
81      // /////////////////////// PRIVATE ATTRIBUTES
82      // ///////////////////////////////////////
83      // ////////////////////////////////////////////////////////////////////////////////////
84  
85      // ////////////////////////////////////////////////////////////////////////////////////
86      // /////////////////////// PRIVATE ATTRIBUTES
87      // ///////////////////////////////////////
88      // ////////////////////////////////////////////////////////////////////////////////////
89  
90      /**
91       * The output stream, where the current file should be written. This output
92       * stream should not be used. The buffered one is much faster.
93       */
94      private OutputStream ftpOutputStream = null;
95  
96      /**
97       * The buffered stream, that the application should use for upload.
98       */
99      private BufferedOutputStream bufferedOutputStream = null;
100 
101     private Matcher uriMatch;
102 
103     // the client that does the actual connecting to the server
104     private FTPClient ftp = new FTPClient();
105 
106     /** FTP user, taken from the postURL applet parameter */
107     private String user;
108 
109     /** FTP password, taken from the postURL applet parameter */
110     private String pass;
111 
112     /** FTP target host, taken from the postURL applet parameter */
113     private String host;
114 
115     /** FTP target port, taken from the postURL applet parameter */
116     private String port;
117 
118     /**
119      * FTP target root folder for the upload, taken from the postURL applet
120      * parameter
121      */
122     private String ftpRootFolder;
123 
124     /**
125      * Indicates whether the connection to the FTP server is open or not. This
126      * allows to connect once on the FTP server, for multiple file upload.
127      */
128     private boolean bConnected = false;
129 
130     /**
131      * This pattern defines the groups and pattern of the ftp syntax.
132      */
133     public final Pattern ftpPattern = Pattern
134             .compile("^ftp://(([^:]+):([^\\@]+)\\@)?([^/:]+):?([0-9]+)?(/(.*))?$");
135 
136     /**
137      * Creates a new instance. Performs the connection to the server based on
138      * the matcher created in the main.
139      * 
140      * @param uploadPolicy
141      * @param packetQueue The queue from wich packets to upload are available.
142      * @param fileUploadManagerThread
143      * @throws JUploadException
144      * @throws IllegalArgumentException if any error occurs. message is error
145      */
146     public FileUploadThreadFTP(UploadPolicy uploadPolicy,
147             BlockingQueue<UploadFilePacket> packetQueue,
148             FileUploadManagerThread fileUploadManagerThread)
149             throws JUploadException {
150         super("FileUploadThreadFTP thread", packetQueue, uploadPolicy,
151                 fileUploadManagerThread);
152         this.uploadPolicy.displayDebug("[FileUploadThreadFTP]  Using "
153                 + this.getClass().getName(), 30);
154 
155         // Some coherence checks, for parameter given to the applet.
156 
157         // stringUploadSuccess: unused in FTP mode. Must be null.
158         if (uploadPolicy.getStringUploadSuccess() != null) {
159             uploadPolicy
160                     .displayWarn("FTP mode: stringUploadSuccess parameter ignored (forced to null)");
161             uploadPolicy.setProperty(UploadPolicy.PROP_STRING_UPLOAD_SUCCESS,
162                     null);
163         }
164 
165         // nbFilesPerRequest: must be 1 in FTP mode.
166         if (uploadPolicy.getNbFilesPerRequest() != 1) {
167             uploadPolicy
168                     .displayWarn("FTP mode: nbFilesPerRequest parameter ignored (forced to 1)");
169             uploadPolicy.setProperty(UploadPolicy.PROP_NB_FILES_PER_REQUEST,
170                     "1");
171         }
172 
173         // maxChunkSize: must be unlimited (no chunk management in FTP mode).
174         if (uploadPolicy.getMaxChunkSize() != Long.MAX_VALUE) {
175             uploadPolicy
176                     .displayWarn("FTP mode: maxChunkSize parameter ignored (forced to Long.MAX_VALUE)");
177             uploadPolicy.setProperty(UploadPolicy.PROP_MAX_CHUNK_SIZE,
178                     Long.toString(Long.MAX_VALUE));
179         }
180     }
181 
182     /** @see DefaultFileUploadThread#beforeRequest(UploadFilePacket) */
183     @Override
184     void beforeRequest(UploadFilePacket packet) throws JUploadException {
185 
186         // If we're connected, we need to check the connection.
187         if (this.bConnected) {
188             // Let's check the connection is still Ok.
189             try {
190                 this.ftp.sendNoOp();
191             } catch (FTPConnectionClosedException eClosed) {
192                 // Let's forget this connection.
193                 this.bConnected = false;
194             } catch (IOException e) {
195                 // commons-net seems to more generate a specific exception, once
196                 // migrated from 1.4.1 to 2.2.
197                 // Let's check the error message. Hope it won't change,
198                 // depending on the user settings..
199                 if (e.getMessage().equals("Connection is not open")) {
200                     // Let's forget this connection.
201                     this.bConnected = false;
202                 } else {
203                     throw new JUploadIOException(e.getClass().getName()
204                             + " while checking FTP connection to the server", e);
205                 }
206             }
207         }
208 
209         // If not already connected ... we connect to the server.
210         if (!this.bConnected) {
211             // Let's connect to the FTP server.
212             String url = this.uploadPolicy.getPostURL();
213             this.uriMatch = this.ftpPattern.matcher(url);
214             if (!this.uriMatch.matches()) {
215                 throw new JUploadException("invalid URI: " + url);
216             }
217             this.user = this.uriMatch.group(2) == null ? "anonymous"
218                     : this.uriMatch.group(2);
219             this.pass = this.uriMatch.group(3) == null ? "JUpload"
220                     : this.uriMatch.group(3);
221             this.host = this.uriMatch.group(4); // no default server
222             this.port = this.uriMatch.group(5) == null ? "21" : this.uriMatch
223                     .group(5);
224             this.ftpRootFolder = (this.uriMatch.group(7) == null) ? null : "/"
225                     + this.uriMatch.group(7);
226             // The last character must be a slash
227             if (this.ftpRootFolder != null && !this.ftpRootFolder.endsWith("/")) {
228                 this.ftpRootFolder += "/";
229             }
230 
231             // do connect.. any error will be thrown up the chain
232             try {
233                 this.ftp.setDefaultPort(Integer.parseInt(this.port));
234                 this.ftp.connect(this.host);
235                 this.uploadPolicy.displayDebug(
236                         "[FileUploadThreadFTP] Connected to " + this.host, 10);
237                 this.uploadPolicy.displayDebug(this.ftp.getReplyString(), 80);
238 
239                 if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode()))
240                     throw new JUploadException("FTP server refused connection.");
241 
242                 // given the login information, do the login
243                 this.ftp.login(this.user, this.pass);
244                 this.uploadPolicy.displayDebug("[FileUploadThreadFTP] "
245                         + this.ftp.getReplyString(), 80);
246 
247                 if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode()))
248                     throw new JUploadException(
249                             "Invalid ftp username / password");
250 
251                 this.bConnected = true;
252             } catch (JUploadException jue) {
253                 // No special action, we keep the exception untouched
254                 throw jue;
255             } catch (IOException ioe) {
256                 throw new JUploadIOException(ioe.getClass().getName()
257                         + "[FTP] Could not connect to server ("
258                         + ioe.getMessage() + ")", ioe);
259             } catch (Exception e) {
260                 throw new JUploadException(e.getClass().getName()
261                         + "[FTP] Could not connect to server ("
262                         + e.getMessage() + ")", e);
263             }
264 
265             // now do the same for the passive/active parameter
266             if (this.uploadPolicy.getFtpTransfertPassive()) {
267                 this.ftp.enterLocalPassiveMode();
268             } else {
269                 this.ftp.enterLocalActiveMode();
270             }
271 
272         } // if(!bConnected)
273     }
274 
275     /** @see DefaultFileUploadThread#afterFile(UploadFileData) */
276     @Override
277     void afterFile(UploadFileData uploadFileData) {
278         // Nothing to do
279     }
280 
281     /** @see DefaultFileUploadThread#beforeFile(UploadFilePacket, UploadFileData) */
282     @Override
283     void beforeFile(UploadFilePacket uploadFilePacket,
284             UploadFileData uploadFileData) throws JUploadException {
285         String workingDir = null;
286         String action = "Starting beforeFile";
287         try {
288             // if configured to, we go to the relative sub-folder of the current
289             // file, or on the root of the postURL.
290             if (this.uploadPolicy.getFtpCreateDirectoryStructure()) {
291                 // We create the FTP directory structure
292                 // TODO: call it once for all files, not once for each file.
293                 action = "Before createDirectoryStructure";
294                 createDirectoryStructure(uploadFilePacket);
295 
296                 workingDir = this.ftpRootFolder
297                         + uploadFileData.getRelativeDir();
298                 // We want to have only slashes, as anti-slashes may generate
299                 // FTP errors.
300                 workingDir = workingDir.replace("\\", "/");
301 
302                 this.uploadPolicy
303                         .displayDebug(
304                                 "[FileUploadThreadFTP] ftpCreateDirectoryStructure: Changing working directory to: "
305                                         + workingDir, 80);
306             } else {
307                 workingDir = this.ftpRootFolder;
308             }
309 
310             if (workingDir != null && !workingDir.equals("")
311                     && !workingDir.equals(".")) {
312                 action = "Before changeWorkingDirectory";
313                 this.ftp.changeWorkingDirectory(workingDir);
314                 action = "After changeWorkingDirectory";
315                 this.uploadPolicy.displayDebug("[FileUploadThreadFTP] "
316                         + this.ftp.getReplyString(), 80);
317             }
318 
319             if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode())) {
320                 throw new JUploadException(
321                         "[FTP] Error while changing directory to: "
322                                 + workingDir + " (" + this.ftp.getReplyString()
323                                 + ")");
324             }
325 
326             action = "Before setTransferType";
327             setTransferType(uploadFileData);
328             action = "After setTransferType";
329 
330             // No delete, as the user may not have the right for that. We use,
331             // later, the store command:
332             // If the file already exists, it will be replaced.
333             // ftp.deleteFile(filesToUpload[index].getFileName());
334 
335             // Let's open the stream for this file.
336             action = "Before storeFileStream";
337             this.ftpOutputStream = this.ftp.storeFileStream(uploadFileData
338                     .getFileName());
339             action = "After storeFileStream";
340             if (this.ftpOutputStream == null) {
341                 throw new JUploadIOException(
342                         "Stream connection to the server error. Check that your path on the URL is valid. postURL used is: "
343                                 + this.uploadPolicy.getPostURL());
344             }
345             // The upload is done through a BufferedOutputStream. This speed up
346             // the upload in an unbelievable way ...
347             this.bufferedOutputStream = new BufferedOutputStream(
348                     this.ftpOutputStream);
349         } catch (IOException e) {
350             throw new JUploadIOException(e.getClass().getName()
351                     + " in the action: " + action, e);
352         }
353     }
354 
355     /** @see DefaultFileUploadThread#cleanAll() */
356     @Override
357     void cleanAll() {
358         try {
359             if (this.ftp.isConnected()) {
360                 this.ftp.disconnect();
361                 this.uploadPolicy.displayDebug(
362                         "[FileUploadThreadFTP] disconnected", 50);
363             }
364         } catch (IOException e) {
365             // then we arent connected
366             this.uploadPolicy.displayDebug(
367                     "[FileUploadThreadFTP] Not connected", 50);
368         } finally {
369             this.ftpOutputStream = null;
370             this.bufferedOutputStream = null;
371         }
372     }
373 
374     /** @see DefaultFileUploadThread#cleanRequest() */
375     @Override
376     void cleanRequest() throws JUploadException {
377         if (this.bufferedOutputStream != null) {
378             try {
379                 this.bufferedOutputStream.close();
380                 this.ftpOutputStream.close();
381                 if (!this.ftp.completePendingCommand()) {
382                     throw new JUploadExceptionUploadFailed(
383                             "ftp.completePendingCommand() returned false");
384                 }
385             } catch (IOException e) {
386                 throw new JUploadException(e);
387             } finally {
388                 this.bufferedOutputStream = null;
389             }
390         }
391     }
392 
393     /**
394      * @throws JUploadIOException
395      * @see DefaultFileUploadThread#finishRequest()
396      */
397     @Override
398     int finishRequest() throws JUploadException {
399         try {
400             getOutputStream().flush();
401             return 200;
402         } catch (IOException ioe) {
403             throw new JUploadIOException("FileUploadThreadFTP.finishRequest()",
404                     ioe);
405         } catch (Exception e) {
406             // When the user may not override an existing file, I got a
407             // NullPointerException. Let's trap all errors here.
408             throw new JUploadException(
409                     "FileUploadThreadFTP.finishRequest()  (check the user permission on the server)",
410                     e);
411         }
412     }
413 
414     /** @see DefaultFileUploadThread#getAdditionnalBytesForUpload(UploadFileData) */
415     @Override
416     long getAdditionnalBytesForUpload(UploadFileData uploadFileData) {
417         // Default: no additional byte.
418         return 0;
419     }
420 
421     /** @see DefaultFileUploadThread#getOutputStream() */
422     @Override
423     OutputStream getOutputStream() {
424         return this.bufferedOutputStream;
425     }
426 
427     /** @see DefaultFileUploadThread#startRequest(long, boolean, int, boolean) */
428     @Override
429     void startRequest(long contentLength, boolean bChunkEnabled, int chunkPart,
430             boolean bLastChunk) {
431         // Nothing to do
432     }
433 
434     /**
435      * Will set the binary/ascii value based on the parameters to the applet.
436      * This could be done by file extension too but it is not implemented.
437      * 
438      * @param uploadFileData The file that we want to upload.
439      * @throws IOException if an error occurs while setting mode data
440      */
441     private void setTransferType(UploadFileData uploadFileData)
442             throws JUploadIOException {
443         try {
444             // read the value given from the user
445             if (this.uploadPolicy.getFtpTransfertBinary()) {
446                 this.ftp.setFileType(FTP.BINARY_FILE_TYPE);
447             } else {
448                 this.ftp.setFileType(FTP.ASCII_FILE_TYPE);
449             }
450         } catch (IOException ioe) {
451             throw new JUploadIOException(
452                     "Cannot set transfert binary or ascii mode (binary: "
453                             + this.uploadPolicy.getFtpTransfertBinary() + ")",
454                     ioe);
455         }
456     }
457 
458     /**
459      * Create all relative sub-directories, so the structure on the server
460      * reflects the structure of the uploaded files.
461      * 
462      * @throws JUploadIOException When an error occurs during folder creation
463      */
464     // A tester
465     private void createDirectoryStructure(UploadFilePacket packet)
466             throws JUploadIOException {
467         SortedSet<String> foldersToCreate = new TreeSet<String>();
468         String folderName;
469         String intermediateFolderName;
470         StringTokenizer st;
471 
472         // 1) Let's find all folders and sub-folders we'll have to create.
473         for (UploadFileData uploadFileData : packet) {
474             if (isInterrupted()) {
475                 break;
476             }
477             folderName = uploadFileData.getRelativeDir();
478             folderName = folderName.replaceAll("\\\\", "/");
479             // Do we already have this folder ?
480             if (!foldersToCreate.contains(folderName)) {
481                 // We add this folder, and all missing intermediate ones
482                 st = new StringTokenizer(folderName, "/");
483                 intermediateFolderName = this.ftpRootFolder;
484                 while (st.hasMoreTokens()) {
485                     intermediateFolderName += st.nextToken() + "/";
486                     if (!foldersToCreate.contains(intermediateFolderName)) {
487                         this.uploadPolicy.displayDebug(
488                                 "FTP structure identification: Adding subfolder "
489                                         + intermediateFolderName, 80);
490                         foldersToCreate.add(intermediateFolderName);
491                     }
492                 }
493             }
494         }
495 
496         // 2) Let's create theses folders.
497         try {
498             String folder;
499             for (Iterator<String> it = foldersToCreate.iterator(); it.hasNext();) {
500                 folder = it.next();
501 
502                 // The folder is in the list of folder to create, created from
503                 // the file list.
504                 // We first check if the folder already exist.
505                 this.ftp.changeWorkingDirectory(folder);
506                 if (FTPReply.isPositiveCompletion(this.ftp.getReplyCode())) {
507                     this.uploadPolicy.displayDebug(
508                             "[FileUploadThreadFTP] Folder " + folder
509                                     + " already exist", 80);
510                 } else {
511                     // We can not guess if it's because the folder
512                     // doesn't exist, or if it's a 'real' error.
513                     // Let's try to create the folder.
514                     this.ftp.mkd(folder);
515                     this.uploadPolicy.displayDebug(
516                             "[FileUploadThreadFTP] Folder " + folder
517                                     + " created", 80);
518                     if (!FTPReply.isPositiveCompletion(this.ftp.getReplyCode())) {
519                         throw new JUploadIOException(
520                                 "Error while creating folder '"
521                                         + folder
522                                         + "' ("
523                                         + this.ftp.getReplyString().replaceAll(
524                                                 "\r\n", "") + ")");
525                     }
526                 }
527             }
528         } catch (IOException ioe) {
529             throw new JUploadIOException(ioe.getClass().getName()
530                     + " in FileUploadThreadFTP.createDirectoryStructure()", ioe);
531         }
532     }
533 
534     /** {@inheritDoc} */
535     @Override
536     void interruptionReceived() {
537         cleanAll();
538     }
539 }