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 }