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}