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