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