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