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     */
017    package org.apache.camel.component.file.remote;
018    
019    import java.io.ByteArrayOutputStream;
020    import java.io.File;
021    import java.io.FileOutputStream;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.OutputStream;
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.List;
028    
029    import org.apache.camel.Exchange;
030    import org.apache.camel.component.file.FileComponent;
031    import org.apache.camel.component.file.GenericFile;
032    import org.apache.camel.component.file.GenericFileEndpoint;
033    import org.apache.camel.component.file.GenericFileExist;
034    import org.apache.camel.component.file.GenericFileOperationFailedException;
035    import org.apache.camel.util.FileUtil;
036    import org.apache.camel.util.ObjectHelper;
037    import org.apache.commons.logging.Log;
038    import org.apache.commons.logging.LogFactory;
039    import org.apache.commons.net.ftp.FTPClient;
040    import org.apache.commons.net.ftp.FTPFile;
041    import org.apache.commons.net.ftp.FTPReply;
042    
043    /**
044     * FTP remote file operations
045     */
046    public class FtpOperations implements RemoteFileOperations<FTPFile> {
047        private static final transient Log LOG = LogFactory.getLog(FtpOperations.class);
048        private final FTPClient client;
049        private RemoteFileEndpoint endpoint;
050    
051        public FtpOperations() {
052            this.client = new FTPClient();
053        }
054    
055        public FtpOperations(FTPClient client) {
056            this.client = client;
057        }
058    
059        public void setEndpoint(GenericFileEndpoint endpoint) {
060            this.endpoint = (RemoteFileEndpoint) endpoint;
061        }
062    
063        public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException {
064            if (LOG.isTraceEnabled()) {
065                LOG.trace("Connecting using FTPClient: " + client);
066            }
067    
068            String host = configuration.getHost();
069            int port = configuration.getPort();
070            String username = configuration.getUsername();
071    
072            FtpConfiguration ftpConfig = (FtpConfiguration) configuration;
073    
074            if (ftpConfig.getFtpClientConfig() != null) {
075                LOG.trace("Configuring FTPClient with config: " + ftpConfig.getFtpClientConfig());
076                client.configure(ftpConfig.getFtpClientConfig());
077            }
078    
079            if (LOG.isTraceEnabled()) {
080                LOG.trace("Connecting to " + configuration.remoteServerInformation());
081            }
082    
083            boolean connected = false;
084            int attempt = 0;
085    
086            while (!connected) {
087                try {
088                    if (LOG.isTraceEnabled() && attempt > 0) {
089                        LOG.trace("Reconnect attempt #" + attempt + " connecting to + " + configuration.remoteServerInformation());
090                    }
091                    client.connect(host, port);
092                    // must check reply code if we are connected
093                    int reply = client.getReplyCode();
094    
095                    if (FTPReply.isPositiveCompletion(reply)) {
096                        // yes we could connect
097                        connected = true;
098                    } else {
099                        // throw an exception to force the retry logic in the catch exception block
100                        throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), "Server refused connection");
101                    }
102                } catch (Exception e) {
103                    GenericFileOperationFailedException failed;
104                    if (e instanceof GenericFileOperationFailedException) {
105                        failed = (GenericFileOperationFailedException) e;
106                    } else {
107                        failed = new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
108                    }
109    
110                    if (LOG.isTraceEnabled()) {
111                        LOG.trace("Could not connect due: " + failed.getMessage());
112                    }
113                    attempt++;
114                    if (attempt > endpoint.getMaximumReconnectAttempts()) {
115                        throw failed;
116                    }
117                    if (endpoint.getReconnectDelay() > 0) {
118                        try {
119                            Thread.sleep(endpoint.getReconnectDelay());
120                        } catch (InterruptedException ie) {
121                            // ignore
122                        }
123                    }
124                }
125            }
126    
127            // must enter passive mode directly after connect
128            if (configuration.isPassiveMode()) {
129                LOG.trace("Using passive mode connections");
130                client.enterLocalPassiveMode();
131            }
132    
133            try {
134                boolean login;
135                if (username != null) {
136                    if (LOG.isTraceEnabled()) {
137                        LOG.trace("Attempting to login user: " + username + " using password: " + configuration.getPassword());
138                    }
139                    login = client.login(username, configuration.getPassword());
140                } else {
141                    LOG.trace("Attempting to login anonymous");
142                    login = client.login("anonymous", null);
143                }
144                if (LOG.isTraceEnabled()) {
145                    LOG.trace("User " + (username != null ? username : "anonymous") + " logged in: " + login);
146                }
147                if (!login) {
148                    throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString());
149                }
150                client.setFileType(configuration.isBinary() ? FTPClient.BINARY_FILE_TYPE : FTPClient.ASCII_FILE_TYPE);
151            } catch (IOException e) {
152                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
153            }
154    
155            return true;
156        }
157    
158        public boolean isConnected() throws GenericFileOperationFailedException {
159            return client.isConnected();
160        }
161    
162        public void disconnect() throws GenericFileOperationFailedException {
163            // logout before disconnecting
164            try {
165                client.logout();
166            } catch (IOException e) {
167                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
168            } finally {
169                try {
170                    client.disconnect();
171                } catch (IOException e) {
172                    throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
173                }
174            }
175        }
176    
177        public boolean deleteFile(String name) throws GenericFileOperationFailedException {
178            if (LOG.isDebugEnabled()) {
179                LOG.debug("Deleteing file: " + name);
180            }
181            try {
182                return this.client.deleteFile(name);
183            } catch (IOException e) {
184                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
185            }
186        }
187    
188        public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
189            if (LOG.isDebugEnabled()) {
190                LOG.debug("Renaming file: " + from + " to: " + to);
191            }
192            try {
193                return client.rename(from, to);
194            } catch (IOException e) {
195                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
196            }
197        }
198    
199        public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
200            if (LOG.isTraceEnabled()) {
201                LOG.trace("Building directory: " + directory);
202            }
203            try {
204                String originalDirectory = client.printWorkingDirectory();
205    
206                boolean success;
207                try {
208                    // maybe the full directory already exsits
209                    success = client.changeWorkingDirectory(directory);
210                    if (!success) {
211                        if (LOG.isTraceEnabled()) {
212                            LOG.trace("Trying to build remote directory: " + directory);
213                        }
214                        success = client.makeDirectory(directory);
215                        if (!success) {
216                            // we are here if the server side doesn't create intermediate folders so create the folder one by one
217                            success = buildDirectoryChunks(directory);
218                        }
219                    }
220    
221                    return success;
222                } finally {
223                    // change back to original directory
224                    if (originalDirectory != null) {
225                        client.changeWorkingDirectory(originalDirectory);
226                    }
227                }
228            } catch (IOException e) {
229                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
230            }
231        }
232    
233        public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
234            if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) {
235                // local work directory is configured so we should store file content as files in this local directory
236                return retrieveFileToFileInLocalWorkDirectory(name, exchange);
237            } else {
238                // store file content directory as stream on the body
239                return retrieveFileToStreamInBody(name, exchange);
240            }
241        }
242    
243        private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException {
244            OutputStream os = null;
245            try {
246                os = new ByteArrayOutputStream();
247                GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
248                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
249                target.setBody(os);
250                return client.retrieveFile(name, os);
251            } catch (IOException e) {
252                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
253            } finally {
254                ObjectHelper.close(os, "retrieve: " + name, LOG);
255            }
256        }
257    
258        private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException {
259            File temp;        
260            File local = new File(FileUtil.normalizePath(endpoint.getLocalWorkDirectory()));
261            OutputStream os;
262            try {
263                // use relative filename in local work directory
264                GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
265                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
266                String relativeName = target.getRelativeFilePath();
267                
268                temp = new File(local, relativeName + ".inprogress");
269                local = new File(local, relativeName);
270    
271                // create directory to local work file
272                local.mkdirs();
273    
274                // delete any existing files
275                if (temp.exists()) {
276                    if (!temp.delete()) {
277                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp);
278                    }
279                }
280                if (local.exists()) {
281                    if (!local.delete()) {
282                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local);
283                    }                
284                }
285    
286                // create new temp local work file
287                if (!temp.createNewFile()) {
288                    throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp);
289                }
290    
291                // store content as a file in the local work directory in the temp handle
292                os = new FileOutputStream(temp);
293    
294                // set header with the path to the local work file            
295                exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath());
296    
297            } catch (Exception e) {            
298                throw new GenericFileOperationFailedException("Cannot create new local work file: " + local);
299            }
300    
301            boolean result;
302            try {
303                GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
304                // store the java.io.File handle as the body
305                target.setBody(local);            
306                result = client.retrieveFile(name, os);
307                
308            } catch (IOException e) {            
309                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
310            }  finally {
311                // need to close the stream before rename it
312                ObjectHelper.close(os, "retrieve: " + name, LOG);
313            }   
314                
315            // rename temp to local after we have retrieved the data
316            if (!temp.renameTo(local)) {                
317                throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local);
318            }
319            
320    
321            return result;
322        }
323    
324        public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
325    
326            // if an existing file already exists what should we do?
327            if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) {
328                boolean existFile = existsFile(name);
329                if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) {
330                    // ignore but indicate that the file was written
331                    if (LOG.isTraceEnabled()) {
332                        LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it.");
333                    }
334                    return true;
335                } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) {
336                    throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file.");
337                }
338            }
339    
340            InputStream is = exchange.getIn().getBody(InputStream.class);
341            try {
342                if (endpoint.getFileExist() == GenericFileExist.Append) {
343                    return client.appendFile(name, is);
344                } else {
345                    return client.storeFile(name, is);
346                }
347            } catch (IOException e) {
348                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
349            } finally {
350                ObjectHelper.close(is, "store: " + name, LOG);
351            }
352        }
353    
354        public boolean existsFile(String name) throws GenericFileOperationFailedException {
355            // check whether a file already exists
356            String directory = FileUtil.onlyPath(name);
357            if (directory == null) {
358                return false;
359            }
360    
361            String onlyName = FileUtil.stripPath(name);
362            try {
363                String[] names = client.listNames(directory);
364                for (String existing : names) {
365                    if (existing.equals(onlyName)) {
366                        return true;
367                    }
368                }
369                return false;
370            } catch (IOException e) {
371                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
372            }
373        }
374    
375        public String getCurrentDirectory() throws GenericFileOperationFailedException {
376            try {
377                return client.printWorkingDirectory();
378            } catch (IOException e) {
379                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
380            }
381        }
382    
383        public void changeCurrentDirectory(String newDirectory) throws GenericFileOperationFailedException {
384            try {
385                client.changeWorkingDirectory(newDirectory);
386            } catch (IOException e) {
387                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
388            }
389        }
390    
391        public List<FTPFile> listFiles() throws GenericFileOperationFailedException {
392            return listFiles(".");
393        }
394    
395        public List<FTPFile> listFiles(String path) throws GenericFileOperationFailedException {
396            // use current directory if path not given
397            if (ObjectHelper.isEmpty(path)) {
398                path = ".";
399            }
400    
401            try {
402                final List<FTPFile> list = new ArrayList<FTPFile>();
403                FTPFile[] files = client.listFiles(path);
404                list.addAll(Arrays.asList(files));
405                return list;
406            } catch (IOException e) {
407                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
408            }
409        }
410    
411        private boolean buildDirectoryChunks(String dirName) throws IOException {
412            final StringBuilder sb = new StringBuilder(dirName.length());
413            final String[] dirs = dirName.split("/|\\\\");
414    
415            boolean success = false;
416            for (String dir : dirs) {
417                sb.append(dir).append('/');
418                String directory = sb.toString();
419    
420                // do not try to build root / folder
421                if (!directory.equals("/")) {
422                    if (LOG.isTraceEnabled()) {
423                        LOG.trace("Trying to build remote directory by chunk: " + directory);
424                    }
425    
426                    success = client.makeDirectory(directory);
427                }
428            }
429    
430            return success;
431        }
432        
433    }