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.Iterator;
028    import java.util.List;
029    
030    import org.apache.camel.Exchange;
031    import org.apache.camel.InvalidPayloadException;
032    import org.apache.camel.component.file.FileComponent;
033    import org.apache.camel.component.file.GenericFile;
034    import org.apache.camel.component.file.GenericFileEndpoint;
035    import org.apache.camel.component.file.GenericFileExist;
036    import org.apache.camel.component.file.GenericFileOperationFailedException;
037    import org.apache.camel.util.FileUtil;
038    import org.apache.camel.util.IOHelper;
039    import org.apache.camel.util.ObjectHelper;
040    import org.apache.commons.logging.Log;
041    import org.apache.commons.logging.LogFactory;
042    import org.apache.commons.net.ftp.FTPClient;
043    import org.apache.commons.net.ftp.FTPClientConfig;
044    import org.apache.commons.net.ftp.FTPFile;
045    import org.apache.commons.net.ftp.FTPReply;
046    
047    /**
048     * FTP remote file operations
049     */
050    public class FtpOperations implements RemoteFileOperations<FTPFile> {
051        
052        protected final transient Log log = LogFactory.getLog(getClass());
053        protected final FTPClient client;
054        protected final FTPClientConfig clientConfig;
055        protected RemoteFileEndpoint<FTPFile> endpoint;
056    
057        public FtpOperations(FTPClient client, FTPClientConfig clientConfig) {
058            this.client = client;
059            this.clientConfig = clientConfig;
060        }
061    
062        public void setEndpoint(GenericFileEndpoint<FTPFile> endpoint) {
063            this.endpoint = (RemoteFileEndpoint<FTPFile>) endpoint;
064        }
065    
066        public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException {
067            if (log.isTraceEnabled()) {
068                log.trace("Connecting using FTPClient: " + client);
069            }
070    
071            String host = configuration.getHost();
072            int port = configuration.getPort();
073            String username = configuration.getUsername();
074    
075            if (clientConfig != null) {
076                log.trace("Configuring FTPClient with config: " + clientConfig);
077                client.configure(clientConfig);
078            }
079    
080            if (log.isTraceEnabled()) {
081                log.trace("Connecting to " + configuration.remoteServerInformation() + " using connection timeout: " + client.getConnectTimeout());
082            }
083    
084            boolean connected = false;
085            int attempt = 0;
086    
087            while (!connected) {
088                try {
089                    if (log.isTraceEnabled() && attempt > 0) {
090                        log.trace("Reconnect attempt #" + attempt + " connecting to + " + configuration.remoteServerInformation());
091                    }
092                    client.connect(host, port);
093                    // must check reply code if we are connected
094                    int reply = client.getReplyCode();
095    
096                    if (FTPReply.isPositiveCompletion(reply)) {
097                        // yes we could connect
098                        connected = true;
099                    } else {
100                        // throw an exception to force the retry logic in the catch exception block
101                        throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), "Server refused connection");
102                    }
103                } catch (Exception e) {
104                    // check if we are interrupted so we can break out
105                    if (Thread.currentThread().isInterrupted()) {
106                        throw new GenericFileOperationFailedException("Interrupted during connecting", new InterruptedException("Interrupted during connecting"));
107                    }
108    
109                    GenericFileOperationFailedException failed;
110                    if (e instanceof GenericFileOperationFailedException) {
111                        failed = (GenericFileOperationFailedException) e;
112                    } else {
113                        failed = new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
114                    }
115    
116                    if (log.isTraceEnabled()) {
117                        log.trace("Cannot connect due: " + failed.getMessage());
118                    }
119                    attempt++;
120                    if (attempt > endpoint.getMaximumReconnectAttempts()) {
121                        throw failed;
122                    }
123                    if (endpoint.getReconnectDelay() > 0) {
124                        try {
125                            Thread.sleep(endpoint.getReconnectDelay());
126                        } catch (InterruptedException ie) {
127                            // we could potentially also be interrupted during sleep
128                            Thread.currentThread().interrupt();
129                            throw new GenericFileOperationFailedException("Interrupted during sleeping", ie);
130                        }
131                    }
132                }
133            }
134    
135            // must enter passive mode directly after connect
136            if (configuration.isPassiveMode()) {
137                log.trace("Using passive mode connections");
138                client.enterLocalPassiveMode();
139            }
140    
141            // must set soTimeout after connect
142            if (endpoint instanceof FtpEndpoint) {
143                FtpEndpoint ftpEndpoint = (FtpEndpoint) endpoint;
144                if (ftpEndpoint.getSoTimeout() > 0) {
145                    log.trace("Using SoTimeout=" + ftpEndpoint.getSoTimeout());
146                    try {
147                        client.setSoTimeout(ftpEndpoint.getSoTimeout());
148                    } catch (IOException e) {
149                        throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
150                    }
151                }
152            }
153    
154            try {
155                boolean login;
156                if (username != null) {
157                    if (log.isTraceEnabled()) {
158                        log.trace("Attempting to login user: " + username + " using password: " + configuration.getPassword());
159                    }
160                    login = client.login(username, configuration.getPassword());
161                } else {
162                    log.trace("Attempting to login anonymous");
163                    login = client.login("anonymous", "");
164                }
165                if (log.isTraceEnabled()) {
166                    log.trace("User " + (username != null ? username : "anonymous") + " logged in: " + login);
167                }
168                if (!login) {
169                    throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString());
170                }
171                client.setFileType(configuration.isBinary() ? FTPClient.BINARY_FILE_TYPE : FTPClient.ASCII_FILE_TYPE);
172            } catch (IOException e) {
173                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
174            }
175    
176            // site commands
177            if (endpoint.getConfiguration().getSiteCommand() != null) {
178                // commands can be separated using new line
179                Iterator it = ObjectHelper.createIterator(endpoint.getConfiguration().getSiteCommand(), "\n");
180                while (it.hasNext()) {
181                    Object next = it.next();
182                    String command = endpoint.getCamelContext().getTypeConverter().convertTo(String.class, next);
183                    if (log.isTraceEnabled()) {
184                        log.trace("Site command to send: " + command);
185                    }
186                    if (command != null) {
187                        boolean result = sendSiteCommand(command);
188                        if (!result) {
189                            throw new GenericFileOperationFailedException("Site command: " + command + " returned false");
190                        }
191                    }
192                }
193            }
194    
195            return true;
196        }
197    
198        public boolean isConnected() throws GenericFileOperationFailedException {
199            return client.isConnected();
200        }
201    
202        public void disconnect() throws GenericFileOperationFailedException {
203            // logout before disconnecting
204            try {
205                client.logout();
206            } catch (IOException e) {
207                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
208            } finally {
209                try {
210                    client.disconnect();
211                } catch (IOException e) {
212                    throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
213                }
214            }
215        }
216    
217        public boolean deleteFile(String name) throws GenericFileOperationFailedException {
218            if (log.isDebugEnabled()) {
219                log.debug("Deleting file: " + name);
220            }
221            try {
222                return this.client.deleteFile(name);
223            } catch (IOException e) {
224                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
225            }
226        }
227    
228        public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
229            if (log.isDebugEnabled()) {
230                log.debug("Renaming file: " + from + " to: " + to);
231            }
232            try {
233                return client.rename(from, to);
234            } catch (IOException e) {
235                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
236            }
237        }
238    
239        public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
240            if (log.isTraceEnabled()) {
241                log.trace("buildDirectory(" + directory + ")");
242            }
243            try {
244                String originalDirectory = client.printWorkingDirectory();
245    
246                boolean success;
247                try {
248                    // maybe the full directory already exists
249                    success = client.changeWorkingDirectory(directory);
250                    if (!success) {
251                        if (log.isTraceEnabled()) {
252                            log.trace("Trying to build remote directory: " + directory);
253                        }
254                        success = client.makeDirectory(directory);
255                        if (!success) {
256                            // we are here if the server side doesn't create intermediate folders so create the folder one by one
257                            success = buildDirectoryChunks(directory);
258                        }
259                    }
260    
261                    return success;
262                } finally {
263                    // change back to original directory
264                    if (originalDirectory != null) {
265                        client.changeWorkingDirectory(originalDirectory);
266                    }
267                }
268            } catch (IOException e) {
269                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
270            }
271        }
272    
273        public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
274            if (log.isTraceEnabled()) {
275                log.trace("retrieveFile(" + name + ")");
276            }
277            if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) {
278                // local work directory is configured so we should store file content as files in this local directory
279                return retrieveFileToFileInLocalWorkDirectory(name, exchange);
280            } else {
281                // store file content directory as stream on the body
282                return retrieveFileToStreamInBody(name, exchange);
283            }
284        }
285    
286        @SuppressWarnings("unchecked")
287        private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException {
288            OutputStream os = null;
289            boolean result;
290            try {
291                os = new ByteArrayOutputStream();
292                GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
293                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
294                target.setBody(os);
295    
296                // remember current directory
297                String currentDir = getCurrentDirectory();
298    
299                // change directory to path where the file is to be retrieved
300                // (must do this as some FTP servers cannot retrieve using absolute path)
301                String path = FileUtil.onlyPath(name);
302                if (path != null) {
303                    changeCurrentDirectory(path);
304                }
305                String onlyName = FileUtil.stripPath(name);
306    
307                result = client.retrieveFile(onlyName, os);
308    
309                // change back to current directory
310                changeCurrentDirectory(currentDir);
311    
312            } catch (IOException e) {
313                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
314            } finally {
315                IOHelper.close(os, "retrieve: " + name, log);
316            }
317    
318            return result;
319        }
320    
321        @SuppressWarnings("unchecked")
322        private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException {
323            File temp;        
324            File local = new File(FileUtil.normalizePath(endpoint.getLocalWorkDirectory()));
325            OutputStream os;
326            try {
327                // use relative filename in local work directory
328                GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
329                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
330                String relativeName = target.getRelativeFilePath();
331                
332                temp = new File(local, relativeName + ".inprogress");
333                local = new File(local, relativeName);
334    
335                // create directory to local work file
336                local.mkdirs();
337    
338                // delete any existing files
339                if (temp.exists()) {
340                    if (!FileUtil.deleteFile(temp)) {
341                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp);
342                    }
343                }
344                if (local.exists()) {
345                    if (!FileUtil.deleteFile(local)) {
346                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local);
347                    }                
348                }
349    
350                // create new temp local work file
351                if (!temp.createNewFile()) {
352                    throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp);
353                }
354    
355                // store content as a file in the local work directory in the temp handle
356                os = new FileOutputStream(temp);
357    
358                // set header with the path to the local work file            
359                exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath());
360    
361            } catch (Exception e) {            
362                throw new GenericFileOperationFailedException("Cannot create new local work file: " + local);
363            }
364    
365            boolean result;
366            try {
367                GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
368                // store the java.io.File handle as the body
369                target.setBody(local);
370    
371                // remember current directory
372                String currentDir = getCurrentDirectory();
373    
374                // change directory to path where the file is to be retrieved
375                // (must do this as some FTP servers cannot retrieve using absolute path)
376                String path = FileUtil.onlyPath(name);
377                if (path != null) {
378                    changeCurrentDirectory(path);
379                }
380                String onlyName = FileUtil.stripPath(name);
381    
382                result = client.retrieveFile(onlyName, os);
383                
384                // change back to current directory
385                changeCurrentDirectory(currentDir);
386    
387            } catch (IOException e) {
388                if (log.isTraceEnabled()) {
389                    log.trace("Error occurred during retrieving file: " + name + " to local directory. Deleting local work file: " + temp);
390                }
391                // failed to retrieve the file so we need to close streams and delete in progress file
392                // must close stream before deleting file
393                IOHelper.close(os, "retrieve: " + name, log);
394                boolean deleted = FileUtil.deleteFile(temp);
395                if (!deleted) {
396                    log.warn("Error occurred during retrieving file: " + name + " to local directory. Cannot delete local work file: " + temp);
397                }
398                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
399            } finally {
400                // need to close the stream before rename it
401                IOHelper.close(os, "retrieve: " + name, log);
402            }
403    
404            if (log.isDebugEnabled()) {
405                log.debug("Retrieve file to local work file result: " + result);
406            }
407    
408            if (result) {
409                if (log.isTraceEnabled()) {
410                    log.trace("Renaming local in progress file from: " + temp + " to: " + local);
411                }
412                // operation went okay so rename temp to local after we have retrieved the data
413                if (!FileUtil.renameFile(temp, local)) {
414                    throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local);
415                }
416            }
417    
418            return result;
419        }
420    
421        public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
422            if (log.isTraceEnabled()) {
423                log.trace("storeFile(" + name + ")");
424            }
425    
426            // if an existing file already exists what should we do?
427            if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) {
428                boolean existFile = existsFile(name);
429                if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) {
430                    // ignore but indicate that the file was written
431                    if (log.isTraceEnabled()) {
432                        log.trace("An existing file already exists: " + name + ". Ignore and do not override it.");
433                    }
434                    return true;
435                } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) {
436                    throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file.");
437                }
438            }
439    
440            InputStream is = null;
441            try {
442                is = exchange.getIn().getMandatoryBody(InputStream.class);
443                if (endpoint.getFileExist() == GenericFileExist.Append) {
444                    return client.appendFile(name, is);
445                } else {
446                    return client.storeFile(name, is);
447                }
448            } catch (IOException e) {
449                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
450            } catch (InvalidPayloadException e) {
451                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
452            } finally {
453                IOHelper.close(is, "store: " + name, log);
454            }
455        }
456    
457        public boolean existsFile(String name) throws GenericFileOperationFailedException {
458            if (log.isTraceEnabled()) {
459                log.trace("existsFile(" + name + ")");
460            }
461    
462            // check whether a file already exists
463            String directory = FileUtil.onlyPath(name);
464            String onlyName = FileUtil.stripPath(name);
465            try {
466                String[] names;
467                if (directory != null) {
468                    names = client.listNames(directory);
469                } else {
470                    names = client.listNames();
471                }
472                // can return either null or an empty list depending on FTP servers
473                if (names == null) {
474                    return false;
475                }
476                for (String existing : names) {
477                    if (existing.equals(onlyName)) {
478                        return true;
479                    }
480                }
481                return false;
482            } catch (IOException e) {
483                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
484            }
485        }
486    
487        public String getCurrentDirectory() throws GenericFileOperationFailedException {
488            if (log.isTraceEnabled()) {
489                log.trace("getCurrentDirectory()");
490            }
491            try {
492                return client.printWorkingDirectory();
493            } catch (IOException e) {
494                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
495            }
496        }
497    
498        public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
499            if (ObjectHelper.isEmpty(path)) {
500                return;
501            }
502    
503            // if it starts with the root path then a little special handling for that
504            if (FileUtil.hasLeadingSeparator(path)) {
505                // change to root path
506                doChangeDirectory(path.substring(0, 1));
507                path = path.substring(1);
508            }
509    
510            // split into multiple dirs
511            final String[] dirs = path.split("/|\\\\");
512    
513            if (dirs == null || dirs.length == 0) {
514                // path was just a relative single path
515                doChangeDirectory(path);
516                return;
517            }
518    
519            // there are multiple dirs so do this in chunks
520            for (String dir : dirs) {
521                doChangeDirectory(dir);
522            }
523        }
524    
525        private void doChangeDirectory(String path) {
526            if (path == null || ".".equals(path) || ObjectHelper.isEmpty(path)) {
527                return;
528            }
529    
530            if (log.isTraceEnabled()) {
531                log.trace("Changing directory: " + path);
532            }
533            boolean success;
534            try {
535                success = client.changeWorkingDirectory(path);
536            } catch (IOException e) {
537                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
538            }
539            if (!success) {
540                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), "Cannot change directory to: " + path);
541            }
542        }
543    
544        public void changeToParentDirectory() throws GenericFileOperationFailedException {
545            try {
546                client.changeToParentDirectory();
547            } catch (IOException e) {
548                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
549            }
550        }
551    
552        public List<FTPFile> listFiles() throws GenericFileOperationFailedException {
553            if (log.isTraceEnabled()) {
554                log.trace("listFiles()");
555            }
556            try {
557                final List<FTPFile> list = new ArrayList<FTPFile>();
558                FTPFile[] files = client.listFiles();
559                // can return either null or an empty list depending on FTP servers
560                if (files != null) {
561                    list.addAll(Arrays.asList(files));
562                }
563                return list;
564            } catch (IOException e) {
565                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
566            }
567        }
568    
569        public List<FTPFile> listFiles(String path) throws GenericFileOperationFailedException {
570            if (log.isTraceEnabled()) {
571                log.trace("listFiles(" + path + ")");
572            }
573            // use current directory if path not given
574            if (ObjectHelper.isEmpty(path)) {
575                path = ".";
576            }
577    
578            try {
579                final List<FTPFile> list = new ArrayList<FTPFile>();
580                FTPFile[] files = client.listFiles(path);
581                // can return either null or an empty list depending on FTP servers
582                if (files != null) {
583                    list.addAll(Arrays.asList(files));
584                }
585                return list;
586            } catch (IOException e) {
587                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
588            }
589        }
590    
591        public boolean sendNoop() throws GenericFileOperationFailedException {
592            if (log.isTraceEnabled()) {
593                log.trace("sendNoOp");
594            }
595            try {
596                return client.sendNoOp();
597            } catch (IOException e) {
598                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
599            }
600        }
601    
602        public boolean sendSiteCommand(String command) throws GenericFileOperationFailedException {
603            if (log.isTraceEnabled()) {
604                log.trace("sendSiteCommand(" + command + ")");
605            }
606            try {
607                return client.sendSiteCommand(command);
608            } catch (IOException e) {
609                throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e);
610            }
611        }
612    
613        protected FTPClient getFtpClient() {
614            return client;
615        }
616    
617        private boolean buildDirectoryChunks(String dirName) throws IOException {
618            final StringBuilder sb = new StringBuilder(dirName.length());
619            final String[] dirs = dirName.split("/|\\\\");
620    
621            boolean success = false;
622            for (String dir : dirs) {
623                sb.append(dir).append('/');
624                String directory = sb.toString();
625    
626                // do not try to build root / folder
627                if (!directory.equals("/")) {
628                    if (log.isTraceEnabled()) {
629                        log.trace("Trying to build remote directory by chunk: " + directory);
630                    }
631    
632                    success = client.makeDirectory(directory);
633                }
634            }
635    
636            return success;
637        }
638    
639    }