View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider.ftp;
18  
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.time.Instant;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.commons.net.ftp.FTPClient;
27  import org.apache.commons.net.ftp.FTPConnectionClosedException;
28  import org.apache.commons.net.ftp.FTPFile;
29  import org.apache.commons.net.ftp.FTPReply;
30  import org.apache.commons.vfs2.FileSystemException;
31  import org.apache.commons.vfs2.FileSystemOptions;
32  import org.apache.commons.vfs2.UserAuthenticationData;
33  import org.apache.commons.vfs2.provider.GenericFileName;
34  import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
35  
36  /**
37   * A wrapper to the FTPClient to allow automatic reconnect on connection loss.
38   * <p>
39   * I decided to not to use eg. noop() to determine the state of the connection to avoid unnecessary server round-trips.
40   * </p>
41   */
42  public class FTPClientWrapper implements FtpClient {
43  
44      private static final Log LOG = LogFactory.getLog(FTPClientWrapper.class);
45  
46      /**
47       * Authentication options.
48       */
49      protected final FileSystemOptions fileSystemOptions;
50      private FTPClient ftpClient;
51      private final GenericFileName rootFileName;
52  
53      /**
54       * Constructs a new instance.
55       *
56       * @param rootFileName the root file name.
57       * @param fileSystemOptions the file system options.
58       * @throws FileSystemException if a file system error occurs.
59       */
60      protected FTPClientWrapper(final GenericFileName rootFileName, final FileSystemOptions fileSystemOptions)
61          throws FileSystemException {
62          this.rootFileName = rootFileName;
63          this.fileSystemOptions = fileSystemOptions;
64          getFtpClient(); // fail-fast
65      }
66  
67      @Override
68      public boolean abort() throws IOException {
69          try {
70              // imario@apache.org: 2005-02-14
71              // it should be better to really "abort" the transfer, but
72              // currently I didn't manage to make it work - so lets "abort" the hard way.
73              // return getFtpClient().abort();
74              disconnect();
75              return true;
76          } catch (final IOException e) {
77              disconnect();
78          }
79          return true;
80      }
81  
82      @Override
83      public OutputStream appendFileStream(final String relPath) throws IOException {
84          try {
85              return getFtpClient().appendFileStream(relPath);
86          } catch (final IOException e) {
87              disconnect();
88              return getFtpClient().appendFileStream(relPath);
89          }
90      }
91  
92      @Override
93      public boolean completePendingCommand() throws IOException {
94          if (ftpClient != null) {
95              return getFtpClient().completePendingCommand();
96          }
97          return true;
98      }
99  
100     /**
101      * Creates an FTP client.
102      *
103      * @return a new FTP client.
104      * @throws FileSystemException if an error occurs while establishing a connection.
105      */
106     private FTPClient createClient() throws FileSystemException {
107         final GenericFileName rootName = getRoot();
108         UserAuthenticationData authData = null;
109         try {
110             authData = UserAuthenticatorUtils.authenticate(fileSystemOptions, FtpFileProvider.AUTHENTICATOR_TYPES);
111             return createClient(rootName, authData);
112         } finally {
113             UserAuthenticatorUtils.cleanup(authData);
114         }
115     }
116 
117     /**
118      * Creates an FTPClient.
119      *
120      * @param rootFileName the root file name.
121      * @param authData authentication data.
122      * @return an FTPClient.
123      * @throws FileSystemException if an error occurs while establishing a connection.
124      */
125     protected FTPClient createClient(final GenericFileName rootFileName, final UserAuthenticationData authData)
126         throws FileSystemException {
127         return FtpClientFactory.createConnection(rootFileName.getHostName(), rootFileName.getPort(),
128             UserAuthenticatorUtils.getData(authData, UserAuthenticationData.USERNAME,
129                 UserAuthenticatorUtils.toChar(rootFileName.getUserName())),
130             UserAuthenticatorUtils.getData(authData, UserAuthenticationData.PASSWORD,
131                 UserAuthenticatorUtils.toChar(rootFileName.getPassword())),
132             rootFileName.getPath(), getFileSystemOptions());
133     }
134 
135     @Override
136     public boolean deleteFile(final String relPath) throws IOException {
137         try {
138             return getFtpClient().deleteFile(relPath);
139         } catch (final IOException e) {
140             disconnect();
141             return getFtpClient().deleteFile(relPath);
142         }
143     }
144 
145     @Override
146     public void disconnect() throws IOException {
147         if (ftpClient != null) {
148             try {
149                 ftpClient.quit();
150             } catch (final IOException e) {
151                 LOG.debug("I/O exception while trying to quit, connection likely timed out, ignoring.", e);
152             } finally {
153                 try {
154                     getFtpClient().disconnect();
155                 } catch (final IOException e) {
156                     LOG.warn("I/O exception while trying to disconnect, connection likely closed, ignoring.", e);
157                 } finally {
158                     ftpClient = null;
159                 }
160             }
161         }
162     }
163 
164     /**
165      * Gets the FileSystemOptions.
166      *
167      * @return the FileSystemOptions.
168      */
169     public FileSystemOptions getFileSystemOptions() {
170         return fileSystemOptions;
171     }
172 
173     /**
174      * Package-private for debugging only, consider private.
175      *
176      * @return the actual FTP client.
177      * @throws FileSystemException if an error occurs while establishing a connection.
178      */
179     FTPClient getFtpClient() throws FileSystemException {
180         if (ftpClient == null) {
181             ftpClient = createClient();
182         }
183         return ftpClient;
184     }
185 
186     @Override
187     public int getReplyCode() throws IOException {
188         return getFtpClient().getReplyCode();
189     }
190 
191     @Override
192     public String getReplyString() throws IOException {
193         return getFtpClient().getReplyString();
194     }
195 
196     /**
197      * Gets the root file name.
198      *
199      * @return  the root file name.
200      */
201     public GenericFileName getRoot() {
202         return rootFileName;
203     }
204 
205     /**
206      * {@inheritDoc}
207      */
208     @Override
209     public boolean hasFeature(final String feature) throws IOException {
210         try {
211             return getFtpClient().hasFeature(feature);
212         } catch (final IOException ex) {
213             disconnect();
214             return getFtpClient().hasFeature(feature);
215         }
216     }
217 
218     @Override
219     public boolean isConnected() throws FileSystemException {
220         return ftpClient != null && ftpClient.isConnected();
221     }
222 
223     @Override
224     public FTPFile[] listFiles(final String relPath) throws IOException {
225         try {
226             // VFS-210: return getFtpClient().listFiles(relPath);
227             return listFilesInDirectory(relPath);
228         } catch (final IOException e) {
229             disconnect();
230             return listFilesInDirectory(relPath);
231         }
232     }
233 
234     private FTPFile[] listFilesInDirectory(final String relPath) throws IOException {
235         // VFS-307: no check if we can simply list the files, this might fail if there are spaces in the path
236         FTPFile[] ftpFiles = getFtpClient().listFiles(relPath);
237         if (FTPReply.isPositiveCompletion(getFtpClient().getReplyCode())) {
238             return ftpFiles;
239         }
240 
241         // VFS-307: now try the hard way by cd'ing into the directory, list and cd back
242         // if VFS is required to fallback here the user might experience a real bad FTP performance
243         // as then every list requires 4 FTP commands.
244         String workingDirectory = null;
245         if (relPath != null) {
246             workingDirectory = getFtpClient().printWorkingDirectory();
247             if (!getFtpClient().changeWorkingDirectory(relPath)) {
248                 return null;
249             }
250         }
251 
252         ftpFiles = getFtpClient().listFiles();
253 
254         if (relPath != null && !getFtpClient().changeWorkingDirectory(workingDirectory)) {
255             throw new FileSystemException("vfs.provider.ftp.wrapper/change-work-directory-back.error",
256                 workingDirectory);
257         }
258         return ftpFiles;
259     }
260 
261     @Override
262     public boolean makeDirectory(final String relPath) throws IOException {
263         try {
264             return getFtpClient().makeDirectory(relPath);
265         } catch (final IOException e) {
266             disconnect();
267             return getFtpClient().makeDirectory(relPath);
268         }
269     }
270 
271     /**
272      * {@inheritDoc}
273      */
274     @Override
275     public Instant mdtmInstant(final String relPath) throws IOException {
276         try {
277             return getFtpClient().mdtmCalendar(relPath).toInstant();
278         } catch (final IOException ex) {
279             disconnect();
280             return getFtpClient().mdtmCalendar(relPath).toInstant();
281         }
282     }
283 
284     @Override
285     public boolean removeDirectory(final String relPath) throws IOException {
286         try {
287             return getFtpClient().removeDirectory(relPath);
288         } catch (final IOException e) {
289             disconnect();
290             return getFtpClient().removeDirectory(relPath);
291         }
292     }
293 
294     @Override
295     public boolean rename(final String oldName, final String newName) throws IOException {
296         try {
297             return getFtpClient().rename(oldName, newName);
298         } catch (final IOException e) {
299             disconnect();
300             return getFtpClient().rename(oldName, newName);
301         }
302     }
303 
304     @Override
305     public InputStream retrieveFileStream(final String relPath) throws IOException {
306         try {
307             return getFtpClient().retrieveFileStream(relPath);
308         } catch (final IOException e) {
309             disconnect();
310             return getFtpClient().retrieveFileStream(relPath);
311         }
312     }
313 
314     @Override
315     public InputStream retrieveFileStream(final String relPath, final int bufferSize) throws IOException {
316         try {
317             final FTPClient client = getFtpClient();
318             client.setBufferSize(bufferSize);
319             return client.retrieveFileStream(relPath);
320         } catch (final IOException e) {
321             disconnect();
322             final FTPClient client = getFtpClient();
323             client.setBufferSize(bufferSize);
324             return client.retrieveFileStream(relPath);
325         }
326     }
327 
328     @Override
329     public InputStream retrieveFileStream(final String relPath, final long restartOffset) throws IOException {
330         try {
331             final FTPClient client = getFtpClient();
332             client.setRestartOffset(restartOffset);
333             return client.retrieveFileStream(relPath);
334         } catch (final IOException e) {
335             disconnect();
336             final FTPClient client = getFtpClient();
337             client.setRestartOffset(restartOffset);
338             return client.retrieveFileStream(relPath);
339         }
340     }
341 
342     /**
343      * A convenience method to send the FTP OPTS command to the server, receive the reply, and return the reply code.
344      * <p>
345      * FTP request Syntax:
346      * </p>
347      * <pre>{@code
348      * opts             = opts-cmd SP command-name
349      *                         [ SP command-options ] CRLF
350      * opts-cmd         = "opts"
351      * command-name     = <any FTP command which allows option setting>
352      * command-options  = <format specified by individual FTP command>
353      * }</pre>
354      * @param commandName The OPTS command name.
355      * @param commandOptions The OPTS command options.
356      * @return The reply code received from the server.
357      * @throws FTPConnectionClosedException If the FTP server prematurely closes the connection as a result of the client being idle or some other reason
358      *                                      causing the server to send FTP reply code 421. This exception may be caught either as an IOException or
359      *                                      independently as itself.
360      * @throws IOException                  If an I/O error occurs while either sending the command or receiving the server reply.
361      * @since 2.11.0
362      */
363     public int sendOptions(final String commandName, String commandOptions) throws IOException {
364         // Commons Net 3.12.0
365         // return getFtpClient().opts(commandName, commandOptions);
366         return getFtpClient().sendCommand("OPTS", commandName + ' ' + commandOptions);
367     }
368 
369     @Override
370     public void setBufferSize(final int bufferSize) throws FileSystemException {
371         getFtpClient().setBufferSize(bufferSize);
372     }
373 
374     @Override
375     public OutputStream storeFileStream(final String relPath) throws IOException {
376         try {
377             return getFtpClient().storeFileStream(relPath);
378         } catch (final IOException e) {
379             disconnect();
380             return getFtpClient().storeFileStream(relPath);
381         }
382     }
383 }