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.Hashtable;
027    import java.util.List;
028    import java.util.Vector;
029    
030    import com.jcraft.jsch.ChannelSftp;
031    import com.jcraft.jsch.JSch;
032    import com.jcraft.jsch.JSchException;
033    import com.jcraft.jsch.Session;
034    import com.jcraft.jsch.SftpException;
035    import com.jcraft.jsch.UIKeyboardInteractive;
036    import com.jcraft.jsch.UserInfo;
037    import org.apache.camel.Exchange;
038    import org.apache.camel.InvalidPayloadException;
039    import org.apache.camel.component.file.FileComponent;
040    import org.apache.camel.component.file.GenericFile;
041    import org.apache.camel.component.file.GenericFileEndpoint;
042    import org.apache.camel.component.file.GenericFileExist;
043    import org.apache.camel.component.file.GenericFileOperationFailedException;
044    import org.apache.camel.util.ExchangeHelper;
045    import org.apache.camel.util.FileUtil;
046    import org.apache.camel.util.IOHelper;
047    import org.apache.camel.util.ObjectHelper;
048    import org.slf4j.Logger;
049    import org.slf4j.LoggerFactory;
050    
051    import static org.apache.camel.util.ObjectHelper.isNotEmpty;
052    
053    /**
054     * SFTP remote file operations
055     */
056    public class SftpOperations implements RemoteFileOperations<ChannelSftp.LsEntry> {
057        private static final transient Logger LOG = LoggerFactory.getLogger(SftpOperations.class);
058        private SftpEndpoint endpoint;
059        private ChannelSftp channel;
060        private Session session;
061    
062        /**
063         * Extended user info which supports interactive keyboard mode, by entering the password.
064         */
065        public static interface ExtendedUserInfo extends UserInfo, UIKeyboardInteractive {
066        }
067    
068        public void setEndpoint(GenericFileEndpoint endpoint) {
069            this.endpoint = (SftpEndpoint) endpoint;
070        }
071    
072        public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException {
073            if (isConnected()) {
074                // already connected
075                return true;
076            }
077    
078            boolean connected = false;
079            int attempt = 0;
080    
081            while (!connected) {
082                try {
083                    if (LOG.isTraceEnabled() && attempt > 0) {
084                        LOG.trace("Reconnect attempt #{} connecting to + {}", attempt, configuration.remoteServerInformation());
085                    }
086    
087                    if (channel == null || !channel.isConnected()) {
088                        if (session == null || !session.isConnected()) {
089                            LOG.trace("Session isn't connected, trying to recreate and connect.");
090                            session = createSession(configuration);
091                            if (endpoint.getConfiguration().getConnectTimeout() > 0) {
092                                LOG.trace("Connecting use connectTimeout: " + endpoint.getConfiguration().getConnectTimeout() + " ...");
093                                session.connect(endpoint.getConfiguration().getConnectTimeout());
094                            } else {
095                                LOG.trace("Connecting ...");
096                                session.connect();
097                            }
098                        }
099    
100                        LOG.trace("Channel isn't connected, trying to recreate and connect.");
101                        channel = (ChannelSftp) session.openChannel("sftp");
102    
103                        if (endpoint.getConfiguration().getConnectTimeout() > 0) {
104                            LOG.trace("Connecting use connectTimeout: " + endpoint.getConfiguration().getConnectTimeout() + " ...");
105                            channel.connect(endpoint.getConfiguration().getConnectTimeout());
106                        } else {
107                            LOG.trace("Connecting ...");
108                            channel.connect();
109                        }
110                        LOG.info("Connected to " + configuration.remoteServerInformation());
111                    }
112    
113                    // yes we could connect
114                    connected = true;
115                } catch (Exception e) {
116                    // check if we are interrupted so we can break out
117                    if (Thread.currentThread().isInterrupted()) {
118                        throw new GenericFileOperationFailedException("Interrupted during connecting", new InterruptedException("Interrupted during connecting"));
119                    }
120    
121                    GenericFileOperationFailedException failed = new GenericFileOperationFailedException("Cannot connect to " + configuration.remoteServerInformation(), e);
122                    LOG.trace("Cannot connect due: {}", failed.getMessage());
123                    attempt++;
124                    if (attempt > endpoint.getMaximumReconnectAttempts()) {
125                        throw failed;
126                    }
127                    if (endpoint.getReconnectDelay() > 0) {
128                        try {
129                            Thread.sleep(endpoint.getReconnectDelay());
130                        } catch (InterruptedException ie) {
131                            // we could potentially also be interrupted during sleep
132                            Thread.currentThread().interrupt();
133                            throw new GenericFileOperationFailedException("Interrupted during sleeping", ie);
134                        }
135                    }
136                }
137            }
138    
139            return true;
140        }
141    
142        protected Session createSession(final RemoteFileConfiguration configuration) throws JSchException {
143            final JSch jsch = new JSch();
144            JSch.setLogger(new JSchLogger());
145    
146            SftpConfiguration sftpConfig = (SftpConfiguration) configuration;
147    
148            if (isNotEmpty(sftpConfig.getCiphers())) {
149                LOG.debug("Using ciphers: {}", sftpConfig.getCiphers());
150                Hashtable<String, String> ciphers = new Hashtable<String, String>();
151                ciphers.put("cipher.s2c", sftpConfig.getCiphers());
152                ciphers.put("cipher.c2s", sftpConfig.getCiphers());
153                JSch.setConfig(ciphers);
154            }
155            
156            if (isNotEmpty(sftpConfig.getPrivateKeyFile())) {
157                LOG.debug("Using private keyfile: {}", sftpConfig.getPrivateKeyFile());
158                if (isNotEmpty(sftpConfig.getPrivateKeyFilePassphrase())) {
159                    jsch.addIdentity(sftpConfig.getPrivateKeyFile(), sftpConfig.getPrivateKeyFilePassphrase());
160                } else {
161                    jsch.addIdentity(sftpConfig.getPrivateKeyFile());
162                }
163            }
164    
165            if (isNotEmpty(sftpConfig.getKnownHostsFile())) {
166                LOG.debug("Using knownhosts file: {}", sftpConfig.getKnownHostsFile());
167                jsch.setKnownHosts(sftpConfig.getKnownHostsFile());
168            }
169    
170            final Session session = jsch.getSession(configuration.getUsername(), configuration.getHost(), configuration.getPort());
171    
172            if (isNotEmpty(sftpConfig.getStrictHostKeyChecking())) {
173                LOG.debug("Using StrickHostKeyChecking: {}", sftpConfig.getStrictHostKeyChecking());
174                session.setConfig("StrictHostKeyChecking", sftpConfig.getStrictHostKeyChecking());
175            }
176            
177            session.setServerAliveInterval(sftpConfig.getServerAliveInterval());
178            session.setServerAliveCountMax(sftpConfig.getServerAliveCountMax());
179    
180            // compression
181            if (sftpConfig.getCompression() > 0) {
182                LOG.debug("Using compression: {}", sftpConfig.getCompression());
183                session.setConfig("compression.s2c", "[email protected],zlib,none");
184                session.setConfig("compression.c2s", "[email protected],zlib,none");
185                session.setConfig("compression_level", Integer.toString(sftpConfig.getCompression()));
186            }
187    
188            // set user information
189            session.setUserInfo(new ExtendedUserInfo() {
190                public String getPassphrase() {
191                    return null;
192                }
193    
194                public String getPassword() {
195                    return configuration.getPassword();
196                }
197    
198                public boolean promptPassword(String s) {
199                    return true;
200                }
201    
202                public boolean promptPassphrase(String s) {
203                    return true;
204                }
205    
206                public boolean promptYesNo(String s) {
207                    LOG.warn("Server asks for confirmation (yes|no): " + s + ". Camel will answer no.");
208                    // Return 'false' indicating modification of the hosts file is disabled.
209                    return false;
210                }
211    
212                public void showMessage(String s) {
213                    LOG.trace("Message received from Server: " + s);
214                }
215    
216                public String[] promptKeyboardInteractive(String destination, String name,
217                                                          String instruction, String[] prompt, boolean[] echo) {
218                    return new String[]{configuration.getPassword()};
219                }
220    
221            });
222            return session;
223        }
224    
225        private static final class JSchLogger implements com.jcraft.jsch.Logger {
226    
227            public boolean isEnabled(int level) {
228                switch (level) {
229                case FATAL:
230                    // use ERROR as FATAL
231                    return LOG.isErrorEnabled();
232                case ERROR:
233                    return LOG.isErrorEnabled();
234                case WARN:
235                    return LOG.isWarnEnabled();
236                case INFO:
237                    return LOG.isInfoEnabled();
238                default:
239                    return LOG.isDebugEnabled();
240                }
241            }
242    
243            public void log(int level, String message) {
244                switch (level) {
245                case FATAL:
246                    // use ERROR as FATAL
247                    LOG.error("JSCH -> " + message);
248                    break;
249                case ERROR:
250                    LOG.error("JSCH -> " + message);
251                    break;
252                case WARN:
253                    LOG.warn("JSCH -> " + message);
254                    break;
255                case INFO:
256                    LOG.info("JSCH -> " + message);
257                    break;
258                default:
259                    LOG.debug("JSCH -> " + message);
260                    break;
261                }
262            }
263        }
264    
265        public boolean isConnected() throws GenericFileOperationFailedException {
266            return session != null && session.isConnected() && channel != null && channel.isConnected();
267        }
268    
269        public void disconnect() throws GenericFileOperationFailedException {
270            if (session != null && session.isConnected()) {
271                session.disconnect();
272            }
273            if (channel != null && channel.isConnected()) {
274                channel.disconnect();
275            }
276        }
277    
278        public boolean deleteFile(String name) throws GenericFileOperationFailedException {
279            LOG.debug("Deleting file: {}", name);
280            try {
281                channel.rm(name);
282                return true;
283            } catch (SftpException e) {
284                throw new GenericFileOperationFailedException("Cannot delete file: " + name, e);
285            }
286        }
287    
288        public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
289            LOG.debug("Renaming file: {} to: {}", from, to);
290            try {
291                channel.rename(from, to);
292                return true;
293            } catch (SftpException e) {
294                throw new GenericFileOperationFailedException("Cannot rename file from: " + from + " to: " + to, e);
295            }
296        }
297    
298        public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
299            // must normalize directory first
300            directory = endpoint.getConfiguration().normalizePath(directory);
301    
302            LOG.trace("buildDirectory({},{})", directory, absolute);
303            // ignore absolute as all dirs are relative with FTP
304            boolean success = false;
305    
306            String originalDirectory = getCurrentDirectory();
307            try {
308                // maybe the full directory already exists
309                try {
310                    channel.cd(directory);
311                    success = true;
312                } catch (SftpException e) {
313                    // ignore, we could not change directory so try to create it instead
314                }
315    
316                if (!success) {
317                    LOG.debug("Trying to build remote directory: {}", directory);
318    
319                    try {
320                        channel.mkdir(directory);
321                        success = true;
322                    } catch (SftpException e) {
323                        // we are here if the server side doesn't create intermediate folders
324                        // so create the folder one by one
325                        success = buildDirectoryChunks(directory);
326                    }
327                }
328            } catch (IOException e) {
329                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
330            } catch (SftpException e) {
331                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
332            } finally {
333                // change back to original directory
334                if (originalDirectory != null) {
335                    changeCurrentDirectory(originalDirectory);
336                }
337            }
338    
339            return success;
340        }
341    
342        private boolean buildDirectoryChunks(String dirName) throws IOException, SftpException {
343            final StringBuilder sb = new StringBuilder(dirName.length());
344            final String[] dirs = dirName.split("/|\\\\");
345    
346            boolean success = false;
347            for (String dir : dirs) {
348                sb.append(dir).append('/');
349                // must normalize the directory name
350                String directory = endpoint.getConfiguration().normalizePath(sb.toString());
351    
352                // do not try to build root folder (/ or \)
353                if (!(directory.equals("/") || directory.equals("\\"))) {
354                    try {
355                        LOG.trace("Trying to build remote directory by chunk: {}", directory);
356    
357                        channel.mkdir(directory);
358                        success = true;
359                    } catch (SftpException e) {
360                        // ignore keep trying to create the rest of the path
361                    }
362                }
363            }
364    
365            return success;
366        }
367    
368        public String getCurrentDirectory() throws GenericFileOperationFailedException {
369            LOG.trace("getCurrentDirectory()");
370            try {
371                return channel.pwd();
372            } catch (SftpException e) {
373                throw new GenericFileOperationFailedException("Cannot get current directory", e);
374            }
375        }
376    
377        public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
378            LOG.trace("changeCurrentDirectory({})", path);
379            if (ObjectHelper.isEmpty(path)) {
380                return;
381            }
382    
383            // must compact path so FTP server can traverse correctly
384            path = FileUtil.compactPath(path);
385    
386            // not stepwise should change directory in one operation
387            if (!endpoint.getConfiguration().isStepwise()) {
388                doChangeDirectory(path);
389                return;
390            }
391    
392            // if it starts with the root path then a little special handling for that
393            if (FileUtil.hasLeadingSeparator(path)) {
394                // change to root path
395                doChangeDirectory(path.substring(0, 1));
396                path = path.substring(1);
397            }
398    
399            // split into multiple dirs
400            final String[] dirs = path.split("/|\\\\");
401    
402            if (dirs == null || dirs.length == 0) {
403                // path was just a relative single path
404                doChangeDirectory(path);
405                return;
406            }
407    
408            // there are multiple dirs so do this in chunks
409            for (String dir : dirs) {
410                doChangeDirectory(dir);
411            }
412        }
413    
414        private void doChangeDirectory(String path) {
415            if (path == null || ".".equals(path) || ObjectHelper.isEmpty(path)) {
416                return;
417            }
418    
419            LOG.trace("Changing directory: {}", path);
420            try {
421                channel.cd(path);
422            } catch (SftpException e) {
423                throw new GenericFileOperationFailedException("Cannot change directory to: " + path, e);
424            }
425        }
426    
427        public void changeToParentDirectory() throws GenericFileOperationFailedException {
428            LOG.trace("changeToParentDirectory()");
429            String current = getCurrentDirectory();
430    
431            String parent = FileUtil.compactPath(current + "/..");
432            // must start with absolute
433            if (!parent.startsWith("/")) {
434                parent = "/" + parent;
435            }
436    
437            changeCurrentDirectory(parent);
438        }
439    
440        public List<ChannelSftp.LsEntry> listFiles() throws GenericFileOperationFailedException {
441            return listFiles(".");
442        }
443    
444        public List<ChannelSftp.LsEntry> listFiles(String path) throws GenericFileOperationFailedException {
445            LOG.trace("listFiles({})", path);
446            if (ObjectHelper.isEmpty(path)) {
447                // list current directory if file path is not given
448                path = ".";
449            }
450    
451            try {
452                final List<ChannelSftp.LsEntry> list = new ArrayList<ChannelSftp.LsEntry>();
453                Vector files = channel.ls(path);
454                // can return either null or an empty list depending on FTP servers
455                if (files != null) {
456                    for (Object file : files) {
457                        list.add((ChannelSftp.LsEntry) file);
458                    }
459                }
460                return list;
461            } catch (SftpException e) {
462                throw new GenericFileOperationFailedException("Cannot list directory: " + path, e);
463            }
464        }
465    
466        public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
467            LOG.trace("retrieveFile({})", name);
468            if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) {
469                // local work directory is configured so we should store file content as files in this local directory
470                return retrieveFileToFileInLocalWorkDirectory(name, exchange);
471            } else {
472                // store file content directory as stream on the body
473                return retrieveFileToStreamInBody(name, exchange);
474            }
475        }
476    
477        @SuppressWarnings("unchecked")
478        private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException {
479            OutputStream os = null;
480            try {
481                os = new ByteArrayOutputStream();
482                GenericFile<ChannelSftp.LsEntry> target =
483                        (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
484                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
485                target.setBody(os);
486    
487                String remoteName = name;
488                String currentDir = null;
489                if (endpoint.getConfiguration().isStepwise()) {
490                    // remember current directory
491                    currentDir = getCurrentDirectory();
492    
493                    // change directory to path where the file is to be retrieved
494                    // (must do this as some FTP servers cannot retrieve using absolute path)
495                    String path = FileUtil.onlyPath(name);
496                    if (path != null) {
497                        changeCurrentDirectory(path);
498                    }
499                    // remote name is now only the file name as we just changed directory
500                    remoteName = FileUtil.stripPath(name);
501                }
502    
503                // use input stream which works with Apache SSHD used for testing
504                InputStream is = channel.get(remoteName);
505                IOHelper.copyAndCloseInput(is, os);
506    
507                // change back to current directory
508                if (endpoint.getConfiguration().isStepwise()) {
509                    changeCurrentDirectory(currentDir);
510                }
511    
512                return true;
513            } catch (IOException e) {
514                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
515            } catch (SftpException e) {
516                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
517            } finally {
518                IOHelper.close(os, "retrieve: " + name, LOG);
519            }
520        }
521    
522        @SuppressWarnings("unchecked")
523        private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException {
524            File temp;
525            File local = new File(endpoint.getLocalWorkDirectory());
526            OutputStream os;
527            GenericFile<ChannelSftp.LsEntry> file =
528                    (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
529            ObjectHelper.notNull(file, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
530            try {
531                // use relative filename in local work directory
532                String relativeName = file.getRelativeFilePath();
533    
534                temp = new File(local, relativeName + ".inprogress");
535                local = new File(local, relativeName);
536    
537                // create directory to local work file
538                local.mkdirs();
539    
540                // delete any existing files
541                if (temp.exists()) {
542                    if (!FileUtil.deleteFile(temp)) {
543                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp);
544                    }
545                }
546                if (local.exists()) {
547                    if (!FileUtil.deleteFile(local)) {
548                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local);
549                    }
550                }
551    
552                // create new temp local work file
553                if (!temp.createNewFile()) {
554                    throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp);
555                }
556    
557                // store content as a file in the local work directory in the temp handle
558                os = new FileOutputStream(temp);
559    
560                // set header with the path to the local work file
561                exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath());
562            } catch (Exception e) {
563                throw new GenericFileOperationFailedException("Cannot create new local work file: " + local);
564            }
565    
566            try {
567                // store the java.io.File handle as the body
568                file.setBody(local);
569    
570                String remoteName = name;
571                String currentDir = null;
572                if (endpoint.getConfiguration().isStepwise()) {
573                    // remember current directory
574                    currentDir = getCurrentDirectory();
575    
576                    // change directory to path where the file is to be retrieved
577                    // (must do this as some FTP servers cannot retrieve using absolute path)
578                    String path = FileUtil.onlyPath(name);
579                    if (path != null) {
580                        changeCurrentDirectory(path);
581                    }
582                    // remote name is now only the file name as we just changed directory
583                    remoteName = FileUtil.stripPath(name);
584                }
585    
586                channel.get(remoteName, os);
587    
588                // change back to current directory
589                if (endpoint.getConfiguration().isStepwise()) {
590                    changeCurrentDirectory(currentDir);
591                }
592    
593            } catch (SftpException e) {
594                LOG.trace("Error occurred during retrieving file: {} to local directory. Deleting local work file: {}", name, temp);
595                // failed to retrieve the file so we need to close streams and delete in progress file
596                // must close stream before deleting file
597                IOHelper.close(os, "retrieve: " + name, LOG);
598                boolean deleted = FileUtil.deleteFile(temp);
599                if (!deleted) {
600                    LOG.warn("Error occurred during retrieving file: " + name + " to local directory. Cannot delete local work file: " + temp);
601                }
602                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
603            } finally {
604                IOHelper.close(os, "retrieve: " + name, LOG);
605            }
606    
607            LOG.debug("Retrieve file to local work file result: true");
608    
609            // operation went okay so rename temp to local after we have retrieved the data
610            LOG.trace("Renaming local in progress file from: {} to: {}", temp, local);
611            try {
612                if (!FileUtil.renameFile(temp, local, false)) {
613                    throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local);
614                }
615            } catch (IOException e) {
616                throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local, e);
617            }
618    
619            return true;
620        }
621    
622        public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
623            // must normalize name first
624            name = endpoint.getConfiguration().normalizePath(name);
625    
626            LOG.trace("storeFile({})", name);
627    
628            boolean answer = false;
629            String currentDir = null;
630            String path = FileUtil.onlyPath(name);
631            String targetName = name;
632    
633            try {
634                if (path != null && endpoint.getConfiguration().isStepwise()) {
635                    // must remember current dir so we stay in that directory after the write
636                    currentDir = getCurrentDirectory();
637    
638                    // change to path of name
639                    changeCurrentDirectory(path);
640    
641                    // the target name should be without path, as we have changed directory
642                    targetName = FileUtil.stripPath(name);
643                }
644    
645                // store the file
646                answer = doStoreFile(name, targetName, exchange);
647            } finally {
648                // change back to current directory if we changed directory
649                if (currentDir != null) {
650                    changeCurrentDirectory(currentDir);
651                }
652            }
653    
654            return answer;
655        }
656    
657        private boolean doStoreFile(String name, String targetName, Exchange exchange) throws GenericFileOperationFailedException {
658            LOG.trace("doStoreFile({})", targetName);
659    
660            // if an existing file already exists what should we do?
661            if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) {
662                boolean existFile = existsFile(targetName);
663                if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) {
664                    // ignore but indicate that the file was written
665                    LOG.trace("An existing file already exists: {}. Ignore and do not override it.", name);
666                    return true;
667                } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) {
668                    throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file.");
669                }
670            }
671    
672            InputStream is = null;
673            try {
674                is = ExchangeHelper.getMandatoryInBody(exchange, InputStream.class);
675                if (endpoint.getFileExist() == GenericFileExist.Append) {
676                    channel.put(is, targetName, ChannelSftp.APPEND);
677                } else {
678                    // override is default
679                    channel.put(is, targetName);
680                }
681    
682                // after storing file, we may set chmod on the file
683                String mode = endpoint.getConfiguration().getChmod();
684                if (ObjectHelper.isNotEmpty(mode)) {
685                    // parse to int using 8bit mode
686                    int permissions = Integer.parseInt(mode, 8);
687                    LOG.trace("Setting chmod: {} on file: ", mode, targetName);
688                    channel.chmod(permissions, targetName);
689                }
690    
691                return true;
692            
693            } catch (SftpException e) {
694                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
695            } catch (InvalidPayloadException e) {
696                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
697            } finally {
698                IOHelper.close(is, "store: " + name, LOG);
699            }
700        }
701    
702        public boolean existsFile(String name) throws GenericFileOperationFailedException {
703            LOG.trace("existsFile({})", name);
704            if (endpoint.isFastExistsCheck()) {
705                return fastExistsFile(name);
706            }
707            // check whether a file already exists
708            String directory = FileUtil.onlyPath(name);
709            if (directory == null) {
710                // assume current dir if no path could be extracted
711                directory = ".";
712            }
713            String onlyName = FileUtil.stripPath(name);
714    
715            try {
716                Vector files = channel.ls(directory);
717                // can return either null or an empty list depending on FTP servers
718                if (files == null) {
719                    return false;
720                }
721                for (Object file : files) {
722                    ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) file;
723                    String existing = entry.getFilename();
724                    LOG.trace("Existing file: {}, target file: {}", existing, name);
725                    existing = FileUtil.stripPath(existing);
726                    if (existing != null && existing.equals(onlyName)) {
727                        return true;
728                    }
729                }
730                return false;
731            } catch (SftpException e) {
732                // or an exception can be thrown with id 2 which means file does not exists
733                if (ChannelSftp.SSH_FX_NO_SUCH_FILE == e.id) {
734                    return false;
735                }
736                // otherwise its a more serious error so rethrow
737                throw new GenericFileOperationFailedException(e.getMessage(), e);
738            }
739        }
740    
741        protected boolean fastExistsFile(String name) throws GenericFileOperationFailedException {
742            LOG.trace("fastExistsFile({})", name);
743            try {
744                Vector files = channel.ls(name);
745                if (files == null) {
746                    return false;
747                }
748                return files.size() >= 1;
749            } catch (SftpException e) {
750                // or an exception can be thrown with id 2 which means file does not exists
751                if (ChannelSftp.SSH_FX_NO_SUCH_FILE == e.id) {
752                    return false;
753                }
754                // otherwise its a more serious error so rethrow
755                throw new GenericFileOperationFailedException(e.getMessage(), e);
756            }
757    
758        }
759    
760        public boolean sendNoop() throws GenericFileOperationFailedException {
761            // is not implemented
762            return true;
763        }
764    
765        public boolean sendSiteCommand(String command) throws GenericFileOperationFailedException {
766            // is not implemented
767            return true;
768        }
769    }