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.FTPConnectionClosedException; 028import org.apache.commons.net.ftp.FTPFile; 029import org.apache.commons.net.ftp.FTPReply; 030import org.apache.commons.vfs2.FileSystemException; 031import org.apache.commons.vfs2.FileSystemOptions; 032import org.apache.commons.vfs2.UserAuthenticationData; 033import org.apache.commons.vfs2.provider.GenericFileName; 034import org.apache.commons.vfs2.util.UserAuthenticatorUtils; 035 036/** 037 * A wrapper to the FTPClient to allow automatic reconnect on connection loss. 038 * <p> 039 * I decided to not to use eg. noop() to determine the state of the connection to avoid unnecessary server round-trips. 040 * </p> 041 */ 042public class FTPClientWrapper implements FtpClient { 043 044 private static final Log LOG = LogFactory.getLog(FTPClientWrapper.class); 045 046 /** 047 * Authentication options. 048 */ 049 protected final FileSystemOptions fileSystemOptions; 050 private FTPClient ftpClient; 051 private final GenericFileName rootFileName; 052 053 /** 054 * Constructs a new instance. 055 * 056 * @param rootFileName the root file name. 057 * @param fileSystemOptions the file system options. 058 * @throws FileSystemException if a file system error occurs. 059 */ 060 protected FTPClientWrapper(final GenericFileName rootFileName, final FileSystemOptions fileSystemOptions) 061 throws FileSystemException { 062 this.rootFileName = rootFileName; 063 this.fileSystemOptions = fileSystemOptions; 064 getFtpClient(); // fail-fast 065 } 066 067 @Override 068 public boolean abort() throws IOException { 069 try { 070 // imario@apache.org: 2005-02-14 071 // it should be better to really "abort" the transfer, but 072 // currently I didn't manage to make it work - so lets "abort" the hard way. 073 // return getFtpClient().abort(); 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 return true; 098 } 099 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}