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