001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.vfs2.provider.ftp; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.OutputStream; 022import java.time.Instant; 023 024import org.apache.commons.logging.Log; 025import org.apache.commons.logging.LogFactory; 026import org.apache.commons.net.ftp.FTPClient; 027import org.apache.commons.net.ftp.FTPFile; 028import org.apache.commons.net.ftp.FTPReply; 029import org.apache.commons.vfs2.FileSystemException; 030import org.apache.commons.vfs2.FileSystemOptions; 031import org.apache.commons.vfs2.UserAuthenticationData; 032import org.apache.commons.vfs2.provider.GenericFileName; 033import org.apache.commons.vfs2.util.UserAuthenticatorUtils; 034 035/** 036 * A wrapper to the FTPClient to allow automatic reconnect on connection loss. 037 * <p> 038 * I decided to not to use eg. noop() to determine the state of the connection to avoid unnecessary server round-trips. 039 * </p> 040 */ 041public class FTPClientWrapper implements FtpClient { 042 043 private static final Log LOG = LogFactory.getLog(FTPClientWrapper.class); 044 045 /** 046 * Authentication options. 047 */ 048 protected final FileSystemOptions fileSystemOptions; 049 private FTPClient ftpClient; 050 private final GenericFileName rootFileName; 051 052 /** 053 * Constructs a new instance. 054 * 055 * @param rootFileName the root file name. 056 * @param fileSystemOptions the file system options. 057 * @throws FileSystemException if a file system error occurs. 058 */ 059 protected FTPClientWrapper(final GenericFileName rootFileName, final FileSystemOptions fileSystemOptions) 060 throws FileSystemException { 061 this.rootFileName = rootFileName; 062 this.fileSystemOptions = fileSystemOptions; 063 getFtpClient(); // fail-fast 064 } 065 066 @Override 067 public boolean abort() throws IOException { 068 try { 069 // imario@apache.org: 2005-02-14 070 // it should be better to really "abort" the transfer, but 071 // currently I didn't manage to make it work - so lets "abort" the hard way. 072 // return getFtpClient().abort(); 073 074 disconnect(); 075 return true; 076 } catch (final IOException e) { 077 disconnect(); 078 } 079 return true; 080 } 081 082 @Override 083 public OutputStream appendFileStream(final String relPath) throws IOException { 084 try { 085 return getFtpClient().appendFileStream(relPath); 086 } catch (final IOException e) { 087 disconnect(); 088 return getFtpClient().appendFileStream(relPath); 089 } 090 } 091 092 @Override 093 public boolean completePendingCommand() throws IOException { 094 if (ftpClient != null) { 095 return getFtpClient().completePendingCommand(); 096 } 097 098 return true; 099 } 100 101 private FTPClient createClient() throws FileSystemException { 102 final GenericFileName rootName = getRoot(); 103 104 UserAuthenticationData authData = null; 105 try { 106 authData = UserAuthenticatorUtils.authenticate(fileSystemOptions, FtpFileProvider.AUTHENTICATOR_TYPES); 107 108 return createClient(rootName, authData); 109 } finally { 110 UserAuthenticatorUtils.cleanup(authData); 111 } 112 } 113 114 /** 115 * Creates an FTPClient. 116 * @param rootFileName the root file name. 117 * @param authData authentication data. 118 * @return an FTPClient. 119 * @throws FileSystemException if a file system error occurs. 120 */ 121 protected FTPClient createClient(final GenericFileName rootFileName, final UserAuthenticationData authData) 122 throws FileSystemException { 123 return FtpClientFactory.createConnection(rootFileName.getHostName(), rootFileName.getPort(), 124 UserAuthenticatorUtils.getData(authData, UserAuthenticationData.USERNAME, 125 UserAuthenticatorUtils.toChar(rootFileName.getUserName())), 126 UserAuthenticatorUtils.getData(authData, UserAuthenticationData.PASSWORD, 127 UserAuthenticatorUtils.toChar(rootFileName.getPassword())), 128 rootFileName.getPath(), getFileSystemOptions()); 129 } 130 131 @Override 132 public boolean deleteFile(final String relPath) throws IOException { 133 try { 134 return getFtpClient().deleteFile(relPath); 135 } catch (final IOException e) { 136 disconnect(); 137 return getFtpClient().deleteFile(relPath); 138 } 139 } 140 141 @Override 142 public void disconnect() throws IOException { 143 try { 144 getFtpClient().quit(); 145 } catch (final IOException e) { 146 LOG.debug("I/O exception while trying to quit, probably it's a timed out connection, ignoring.", e); 147 } finally { 148 try { 149 getFtpClient().disconnect(); 150 } catch (final IOException e) { 151 LOG.warn("I/O exception while trying to disconnect, probably it's a closed connection, ignoring.", e); 152 } finally { 153 ftpClient = null; 154 } 155 } 156 } 157 158 /** 159 * Gets the FileSystemOptions. 160 * 161 * @return the FileSystemOptions. 162 */ 163 public FileSystemOptions getFileSystemOptions() { 164 return fileSystemOptions; 165 } 166 167 private FTPClient getFtpClient() throws FileSystemException { 168 if (ftpClient == null) { 169 ftpClient = createClient(); 170 } 171 172 return ftpClient; 173 } 174 175 @Override 176 public int getReplyCode() throws IOException { 177 return getFtpClient().getReplyCode(); 178 } 179 180 @Override 181 public String getReplyString() throws IOException { 182 return getFtpClient().getReplyString(); 183 } 184 185 /** 186 * Gets the root file name. 187 * 188 * @return the root file name. 189 */ 190 public GenericFileName getRoot() { 191 return rootFileName; 192 } 193 194 /** 195 * {@inheritDoc} 196 */ 197 @Override 198 public boolean hasFeature(final String feature) throws IOException { 199 try { 200 return getFtpClient().hasFeature(feature); 201 } catch (final IOException ex) { 202 disconnect(); 203 return getFtpClient().hasFeature(feature); 204 } 205 } 206 207 @Override 208 public boolean isConnected() throws FileSystemException { 209 return ftpClient != null && ftpClient.isConnected(); 210 } 211 212 @Override 213 public FTPFile[] listFiles(final String relPath) throws IOException { 214 try { 215 // VFS-210: return getFtpClient().listFiles(relPath); 216 return listFilesInDirectory(relPath); 217 } catch (final IOException e) { 218 disconnect(); 219 return listFilesInDirectory(relPath); 220 } 221 } 222 223 private FTPFile[] listFilesInDirectory(final String relPath) throws IOException { 224 // VFS-307: no check if we can simply list the files, this might fail if there are spaces in the path 225 FTPFile[] ftpFiles = getFtpClient().listFiles(relPath); 226 if (FTPReply.isPositiveCompletion(getFtpClient().getReplyCode())) { 227 return ftpFiles; 228 } 229 230 // VFS-307: now try the hard way by cd'ing into the directory, list and cd back 231 // if VFS is required to fallback here the user might experience a real bad FTP performance 232 // as then every list requires 4 FTP commands. 233 String workingDirectory = null; 234 if (relPath != null) { 235 workingDirectory = getFtpClient().printWorkingDirectory(); 236 if (!getFtpClient().changeWorkingDirectory(relPath)) { 237 return null; 238 } 239 } 240 241 ftpFiles = getFtpClient().listFiles(); 242 243 if (relPath != null && !getFtpClient().changeWorkingDirectory(workingDirectory)) { 244 throw new FileSystemException("vfs.provider.ftp.wrapper/change-work-directory-back.error", 245 workingDirectory); 246 } 247 return ftpFiles; 248 } 249 250 @Override 251 public boolean makeDirectory(final String relPath) throws IOException { 252 try { 253 return getFtpClient().makeDirectory(relPath); 254 } catch (final IOException e) { 255 disconnect(); 256 return getFtpClient().makeDirectory(relPath); 257 } 258 } 259 260 /** 261 * {@inheritDoc} 262 */ 263 @Override 264 public Instant mdtmInstant(final String relPath) throws IOException { 265 try { 266 return getFtpClient().mdtmCalendar(relPath).toInstant(); 267 } catch (final IOException ex) { 268 disconnect(); 269 return getFtpClient().mdtmCalendar(relPath).toInstant(); 270 } 271 } 272 273 @Override 274 public boolean removeDirectory(final String relPath) throws IOException { 275 try { 276 return getFtpClient().removeDirectory(relPath); 277 } catch (final IOException e) { 278 disconnect(); 279 return getFtpClient().removeDirectory(relPath); 280 } 281 } 282 283 @Override 284 public boolean rename(final String oldName, final String newName) throws IOException { 285 try { 286 return getFtpClient().rename(oldName, newName); 287 } catch (final IOException e) { 288 disconnect(); 289 return getFtpClient().rename(oldName, newName); 290 } 291 } 292 293 @Override 294 public InputStream retrieveFileStream(final String relPath) throws IOException { 295 try { 296 return getFtpClient().retrieveFileStream(relPath); 297 } catch (final IOException e) { 298 disconnect(); 299 return getFtpClient().retrieveFileStream(relPath); 300 } 301 } 302 303 @Override 304 public InputStream retrieveFileStream(final String relPath, final int bufferSize) throws IOException { 305 try { 306 final FTPClient client = getFtpClient(); 307 client.setBufferSize(bufferSize); 308 return client.retrieveFileStream(relPath); 309 } catch (final IOException e) { 310 disconnect(); 311 final FTPClient client = getFtpClient(); 312 client.setBufferSize(bufferSize); 313 return client.retrieveFileStream(relPath); 314 } 315 } 316 317 @Override 318 public InputStream retrieveFileStream(final String relPath, final long restartOffset) throws IOException { 319 try { 320 final FTPClient client = getFtpClient(); 321 client.setRestartOffset(restartOffset); 322 return client.retrieveFileStream(relPath); 323 } catch (final IOException e) { 324 disconnect(); 325 final FTPClient client = getFtpClient(); 326 client.setRestartOffset(restartOffset); 327 return client.retrieveFileStream(relPath); 328 } 329 } 330 331 @Override 332 public void setBufferSize(final int bufferSize) throws FileSystemException { 333 getFtpClient().setBufferSize(bufferSize); 334 } 335 336 @Override 337 public OutputStream storeFileStream(final String relPath) throws IOException { 338 try { 339 return getFtpClient().storeFileStream(relPath); 340 } catch (final IOException e) { 341 disconnect(); 342 return getFtpClient().storeFileStream(relPath); 343 } 344 } 345}