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.FTPFile;
28  import org.apache.commons.net.ftp.FTPReply;
29  import org.apache.commons.vfs2.FileSystemException;
30  import org.apache.commons.vfs2.FileSystemOptions;
31  import org.apache.commons.vfs2.UserAuthenticationData;
32  import org.apache.commons.vfs2.provider.GenericFileName;
33  import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
34  
35  /**
36   * A wrapper to the FTPClient to allow automatic reconnect on connection loss.
37   * <p>
38   * I decided to not to use eg. noop() to determine the state of the connection to avoid unnecessary server round-trips.
39   * </p>
40   */
41  public class FTPClientWrapper implements FtpClient {
42  
43      private static final Log LOG = LogFactory.getLog(FTPClientWrapper.class);
44  
45      protected final FileSystemOptions fileSystemOptions;
46      private FTPClient ftpClient;
47      private final GenericFileName root;
48  
49      protected FTPClientWrapper(final GenericFileName root, final FileSystemOptions fileSystemOptions)
50          throws FileSystemException {
51          this.root = root;
52          this.fileSystemOptions = fileSystemOptions;
53          getFtpClient(); // fail-fast
54      }
55  
56      @Override
57      public boolean abort() throws IOException {
58          try {
59              // imario@apache.org: 2005-02-14
60              // it should be better to really "abort" the transfer, but
61              // currently I didnt manage to make it work - so lets "abort" the hard way.
62              // return getFtpClient().abort();
63  
64              disconnect();
65              return true;
66          } catch (final IOException e) {
67              disconnect();
68          }
69          return true;
70      }
71  
72      @Override
73      public OutputStream appendFileStream(final String relPath) throws IOException {
74          try {
75              return getFtpClient().appendFileStream(relPath);
76          } catch (final IOException e) {
77              disconnect();
78              return getFtpClient().appendFileStream(relPath);
79          }
80      }
81  
82      @Override
83      public boolean completePendingCommand() throws IOException {
84          if (ftpClient != null) {
85              return getFtpClient().completePendingCommand();
86          }
87  
88          return true;
89      }
90  
91      private FTPClient createClient() throws FileSystemException {
92          final GenericFileName rootName = getRoot();
93  
94          UserAuthenticationData authData = null;
95          try {
96              authData = UserAuthenticatorUtils.authenticate(fileSystemOptions, FtpFileProvider.AUTHENTICATOR_TYPES);
97  
98              return createClient(rootName, authData);
99          } finally {
100             UserAuthenticatorUtils.cleanup(authData);
101         }
102     }
103 
104     protected FTPClient createClient(final GenericFileName rootName, final UserAuthenticationData authData)
105         throws FileSystemException {
106         return FtpClientFactory.createConnection(rootName.getHostName(), rootName.getPort(),
107             UserAuthenticatorUtils.getData(authData, UserAuthenticationData.USERNAME,
108                 UserAuthenticatorUtils.toChar(rootName.getUserName())),
109             UserAuthenticatorUtils.getData(authData, UserAuthenticationData.PASSWORD,
110                 UserAuthenticatorUtils.toChar(rootName.getPassword())),
111             rootName.getPath(), getFileSystemOptions());
112     }
113 
114     @Override
115     public boolean deleteFile(final String relPath) throws IOException {
116         try {
117             return getFtpClient().deleteFile(relPath);
118         } catch (final IOException e) {
119             disconnect();
120             return getFtpClient().deleteFile(relPath);
121         }
122     }
123 
124     @Override
125     public void disconnect() throws IOException {
126         try {
127             getFtpClient().quit();
128         } catch (final IOException e) {
129             LOG.debug("I/O exception while trying to quit, probably it's a timed out connection, ignoring.", e);
130         } finally {
131             try {
132                 getFtpClient().disconnect();
133             } catch (final IOException e) {
134                 LOG.warn("I/O exception while trying to disconnect, probably it's a closed connection, ignoring.", e);
135             } finally {
136                 ftpClient = null;
137             }
138         }
139     }
140 
141     public FileSystemOptions getFileSystemOptions() {
142         return fileSystemOptions;
143     }
144 
145     private FTPClient getFtpClient() throws FileSystemException {
146         if (ftpClient == null) {
147             ftpClient = createClient();
148         }
149 
150         return ftpClient;
151     }
152 
153     @Override
154     public int getReplyCode() throws IOException {
155         return getFtpClient().getReplyCode();
156     }
157 
158     @Override
159     public String getReplyString() throws IOException {
160         return getFtpClient().getReplyString();
161     }
162 
163     public GenericFileName getRoot() {
164         return root;
165     }
166 
167     /**
168      * {@inheritDoc}
169      */
170     @Override
171     public boolean hasFeature(final String feature) throws IOException {
172         try {
173             return getFtpClient().hasFeature(feature);
174         } catch (final IOException ex) {
175             disconnect();
176             return getFtpClient().hasFeature(feature);
177         }
178     }
179 
180     @Override
181     public boolean isConnected() throws FileSystemException {
182         return ftpClient != null && ftpClient.isConnected();
183     }
184 
185     @Override
186     public FTPFile[] listFiles(final String relPath) throws IOException {
187         try {
188             // VFS-210: return getFtpClient().listFiles(relPath);
189             return listFilesInDirectory(relPath);
190         } catch (final IOException e) {
191             disconnect();
192             return listFilesInDirectory(relPath);
193         }
194     }
195 
196     private FTPFile[] listFilesInDirectory(final String relPath) throws IOException {
197         // VFS-307: no check if we can simply list the files, this might fail if there are spaces in the path
198         FTPFile[] ftpFiles = getFtpClient().listFiles(relPath);
199         if (FTPReply.isPositiveCompletion(getFtpClient().getReplyCode())) {
200             return ftpFiles;
201         }
202 
203         // VFS-307: now try the hard way by cd'ing into the directory, list and cd back
204         // if VFS is required to fallback here the user might experience a real bad FTP performance
205         // as then every list requires 4 FTP commands.
206         String workingDirectory = null;
207         if (relPath != null) {
208             workingDirectory = getFtpClient().printWorkingDirectory();
209             if (!getFtpClient().changeWorkingDirectory(relPath)) {
210                 return null;
211             }
212         }
213 
214         ftpFiles = getFtpClient().listFiles();
215 
216         if (relPath != null && !getFtpClient().changeWorkingDirectory(workingDirectory)) {
217             throw new FileSystemException("vfs.provider.ftp.wrapper/change-work-directory-back.error",
218                 workingDirectory);
219         }
220         return ftpFiles;
221     }
222 
223     @Override
224     public boolean makeDirectory(final String relPath) throws IOException {
225         try {
226             return getFtpClient().makeDirectory(relPath);
227         } catch (final IOException e) {
228             disconnect();
229             return getFtpClient().makeDirectory(relPath);
230         }
231     }
232 
233     /**
234      * {@inheritDoc}
235      */
236     @Override
237     public Instant mdtmInstant(final String relPath) throws IOException {
238         try {
239             return getFtpClient().mdtmCalendar(relPath).toInstant();
240         } catch (final IOException ex) {
241             disconnect();
242             return getFtpClient().mdtmCalendar(relPath).toInstant();
243         }
244     }
245 
246     @Override
247     public boolean removeDirectory(final String relPath) throws IOException {
248         try {
249             return getFtpClient().removeDirectory(relPath);
250         } catch (final IOException e) {
251             disconnect();
252             return getFtpClient().removeDirectory(relPath);
253         }
254     }
255 
256     @Override
257     public boolean rename(final String oldName, final String newName) throws IOException {
258         try {
259             return getFtpClient().rename(oldName, newName);
260         } catch (final IOException e) {
261             disconnect();
262             return getFtpClient().rename(oldName, newName);
263         }
264     }
265 
266     @Override
267     public InputStream retrieveFileStream(final String relPath) throws IOException {
268         try {
269             return getFtpClient().retrieveFileStream(relPath);
270         } catch (final IOException e) {
271             disconnect();
272             return getFtpClient().retrieveFileStream(relPath);
273         }
274     }
275 
276     @Override
277     public InputStream retrieveFileStream(final String relPath, final int bufferSize) throws IOException {
278         try {
279             final FTPClient client = getFtpClient();
280             client.setBufferSize(bufferSize);
281             return client.retrieveFileStream(relPath);
282         } catch (final IOException e) {
283             disconnect();
284             final FTPClient client = getFtpClient();
285             client.setBufferSize(bufferSize);
286             return client.retrieveFileStream(relPath);
287         }
288     }
289 
290     @Override
291     public InputStream retrieveFileStream(final String relPath, final long restartOffset) throws IOException {
292         try {
293             final FTPClient client = getFtpClient();
294             client.setRestartOffset(restartOffset);
295             return client.retrieveFileStream(relPath);
296         } catch (final IOException e) {
297             disconnect();
298             final FTPClient client = getFtpClient();
299             client.setRestartOffset(restartOffset);
300             return client.retrieveFileStream(relPath);
301         }
302     }
303 
304     @Override
305     public void setBufferSize(final int bufferSize) throws FileSystemException {
306         getFtpClient().setBufferSize(bufferSize);
307     }
308 
309     @Override
310     public OutputStream storeFileStream(final String relPath) throws IOException {
311         try {
312             return getFtpClient().storeFileStream(relPath);
313         } catch (final IOException e) {
314             disconnect();
315             return getFtpClient().storeFileStream(relPath);
316         }
317     }
318 }