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}