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.List;
027    import java.util.Vector;
028    
029    import com.jcraft.jsch.ChannelSftp;
030    import com.jcraft.jsch.JSch;
031    import com.jcraft.jsch.JSchException;
032    import com.jcraft.jsch.Session;
033    import com.jcraft.jsch.SftpException;
034    import com.jcraft.jsch.UserInfo;
035    
036    import org.apache.camel.Exchange;
037    import org.apache.camel.InvalidPayloadException;
038    import org.apache.camel.component.file.FileComponent;
039    import org.apache.camel.component.file.GenericFile;
040    import org.apache.camel.component.file.GenericFileEndpoint;
041    import org.apache.camel.component.file.GenericFileExist;
042    import org.apache.camel.component.file.GenericFileOperationFailedException;
043    import org.apache.camel.util.ExchangeHelper;
044    import org.apache.camel.util.FileUtil;
045    import org.apache.camel.util.ObjectHelper;
046    import org.apache.commons.logging.Log;
047    import org.apache.commons.logging.LogFactory;
048    
049    import static org.apache.camel.util.ObjectHelper.isNotEmpty;
050    
051    /**
052     * SFTP remote file operations
053     */
054    public class SftpOperations implements RemoteFileOperations<ChannelSftp.LsEntry> {
055        private static final transient Log LOG = LogFactory.getLog(SftpOperations.class);
056        private RemoteFileEndpoint endpoint;
057        private ChannelSftp channel;
058        private Session session;
059    
060        public void setEndpoint(GenericFileEndpoint endpoint) {
061            this.endpoint = (RemoteFileEndpoint) endpoint;
062        }
063    
064        public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException {
065            if (isConnected()) {
066                // already connected
067                return true;
068            }
069    
070            boolean connected = false;
071            int attempt = 0;
072    
073            while (!connected) {
074                try {
075                    if (LOG.isTraceEnabled() && attempt > 0) {
076                        LOG.trace("Reconnect attempt #" + attempt + " connecting to + " + configuration.remoteServerInformation());
077                    }
078    
079                    if (channel == null || !channel.isConnected()) {
080                        if (session == null || !session.isConnected()) {
081                            LOG.trace("Session isn't connected, trying to recreate and connect.");
082                            session = createSession(configuration);
083                            session.connect();
084                        }
085                        LOG.trace("Channel isn't connected, trying to recreate and connect.");
086                        channel = (ChannelSftp) session.openChannel("sftp");
087                        channel.connect();
088                        LOG.info("Connected to " + configuration.remoteServerInformation());
089                    }
090    
091                    // yes we could connect
092                    connected = true;
093                } catch (Exception e) {
094                    GenericFileOperationFailedException failed = new GenericFileOperationFailedException("Cannot connect to " + configuration.remoteServerInformation(), e);
095                    if (LOG.isTraceEnabled()) {
096                        LOG.trace("Cannot connect due: " + failed.getMessage());
097                    }
098                    attempt++;
099                    if (attempt > endpoint.getMaximumReconnectAttempts()) {
100                        throw failed;
101                    }
102                    if (endpoint.getReconnectDelay() > 0) {
103                        try {
104                            Thread.sleep(endpoint.getReconnectDelay());
105                        } catch (InterruptedException e1) {
106                            // ignore
107                        }
108                    }
109                }
110            }
111    
112            return true;
113        }
114    
115        protected Session createSession(final RemoteFileConfiguration configuration) throws JSchException {
116            final JSch jsch = new JSch();
117    
118            SftpConfiguration sftpConfig = (SftpConfiguration) configuration;
119    
120            if (isNotEmpty(sftpConfig.getPrivateKeyFile())) {
121                LOG.debug("Using private keyfile: " + sftpConfig.getPrivateKeyFile());
122                if (isNotEmpty(sftpConfig.getPrivateKeyFilePassphrase())) {
123                    jsch.addIdentity(sftpConfig.getPrivateKeyFile(), sftpConfig.getPrivateKeyFilePassphrase());
124                } else {
125                    jsch.addIdentity(sftpConfig.getPrivateKeyFile());
126                }
127            }
128    
129            if (isNotEmpty(sftpConfig.getKnownHostsFile())) {
130                LOG.debug("Using knownhosts file: " + sftpConfig.getKnownHostsFile());
131                jsch.setKnownHosts(sftpConfig.getKnownHostsFile());
132            }
133    
134            final Session session = jsch.getSession(configuration.getUsername(), configuration.getHost(), configuration.getPort());
135    
136            if (isNotEmpty(sftpConfig.getStrictHostKeyChecking())) {
137                LOG.debug("Using StrickHostKeyChecking: " + sftpConfig.getStrictHostKeyChecking());
138                session.setConfig("StrictHostKeyChecking", sftpConfig.getStrictHostKeyChecking());
139            }
140    
141            // set user information
142            session.setUserInfo(new UserInfo() {
143                public String getPassphrase() {
144                    return null;
145                }
146    
147                public String getPassword() {
148                    return configuration.getPassword();
149                }
150    
151                public boolean promptPassword(String s) {
152                    return true;
153                }
154    
155                public boolean promptPassphrase(String s) {
156                    return true;
157                }
158    
159                public boolean promptYesNo(String s) {
160                    LOG.warn("Server asks for confirmation (yes|no): " + s + ". Camel will answer no.");
161                    // Return 'false' indicating modification of the hosts file is disabled.
162                    return false;
163                }
164    
165                public void showMessage(String s) {
166                    LOG.trace("Message received from Server: " + s);
167                }
168            });
169            return session;
170        }
171    
172        public boolean isConnected() throws GenericFileOperationFailedException {
173            return session != null && session.isConnected() && channel != null && channel.isConnected();
174        }
175    
176        public void disconnect() throws GenericFileOperationFailedException {
177            if (session != null && session.isConnected()) {
178                session.disconnect();
179            }
180            if (channel != null && channel.isConnected()) {
181                channel.disconnect();
182            }
183        }
184    
185        public boolean deleteFile(String name) throws GenericFileOperationFailedException {
186            if (LOG.isDebugEnabled()) {
187                LOG.debug("Deleting file: " + name);
188            }
189            try {
190                channel.rm(name);
191                return true;
192            } catch (SftpException e) {
193                throw new GenericFileOperationFailedException("Cannot delete file: " + name, e);
194            }
195        }
196    
197        public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
198            if (LOG.isDebugEnabled()) {
199                LOG.debug("Renaming file: " + from + " to: " + to);
200            }
201            try {
202                channel.rename(from, to);
203                return true;
204            } catch (SftpException e) {
205                throw new GenericFileOperationFailedException("Cannot rename file from: " + from + " to: " + to, e);
206            }
207        }
208    
209        public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
210            // ignore absolute as all dirs are relative with FTP
211            boolean success = false;
212    
213            String originalDirectory = getCurrentDirectory();
214            try {
215                // maybe the full directory already exists
216                try {
217                    channel.cd(directory);
218                    success = true;
219                } catch (SftpException e) {
220                    // ignore, we could not change directory so try to create it instead
221                }
222    
223                if (!success) {
224                    if (LOG.isDebugEnabled()) {
225                        LOG.debug("Trying to build remote directory: " + directory);
226                    }
227    
228                    try {
229                        channel.mkdir(directory);
230                        success = true;
231                    } catch (SftpException e) {
232                        // we are here if the server side doesn't create intermediate folders
233                        // so create the folder one by one
234                        success = buildDirectoryChunks(directory);
235                    }
236                }
237            } catch (IOException e) {
238                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
239            } catch (SftpException e) {
240                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
241            } finally {
242                // change back to original directory
243                if (originalDirectory != null) {
244                    changeCurrentDirectory(originalDirectory);
245                }
246            }
247    
248            return success;
249        }
250    
251        private boolean buildDirectoryChunks(String dirName) throws IOException, SftpException {
252            final StringBuilder sb = new StringBuilder(dirName.length());
253            final String[] dirs = dirName.split("/|\\\\");
254    
255            boolean success = false;
256            for (String dir : dirs) {
257                sb.append(dir).append('/');
258                String directory = sb.toString();
259                if (LOG.isTraceEnabled()) {
260                    LOG.trace("Trying to build remote directory by chunk: " + directory);
261                }
262    
263                // do not try to build root / folder
264                if (!directory.equals("/")) {
265                    try {
266                        channel.mkdir(directory);
267                        success = true;
268                    } catch (SftpException e) {
269                        // ignore keep trying to create the rest of the path
270                    }
271                }
272            }
273    
274            return success;
275        }
276    
277        public String getCurrentDirectory() throws GenericFileOperationFailedException {
278            try {
279                return channel.pwd();
280            } catch (SftpException e) {
281                throw new GenericFileOperationFailedException("Cannot get current directory", e);
282            }
283        }
284    
285        public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
286            try {
287                channel.cd(path);
288            } catch (SftpException e) {
289                throw new GenericFileOperationFailedException("Cannot change current directory to: " + path, e);
290            }
291        }
292    
293        public List<ChannelSftp.LsEntry> listFiles() throws GenericFileOperationFailedException {
294            return listFiles(".");
295        }
296    
297        public List<ChannelSftp.LsEntry> listFiles(String path) throws GenericFileOperationFailedException {
298            if (ObjectHelper.isEmpty(path)) {
299                // list current directory if file path is not given
300                path = ".";
301            }
302    
303            try {
304                final List<ChannelSftp.LsEntry> list = new ArrayList<ChannelSftp.LsEntry>();
305                Vector files = channel.ls(path);
306                // can return either null or an empty list depending on FTP servers
307                if (files != null) {
308                    for (Object file : files) {
309                        list.add((ChannelSftp.LsEntry)file);
310                    }
311                }
312                return list;
313            } catch (SftpException e) {
314                throw new GenericFileOperationFailedException("Cannot list directory: " + path, e);
315            }
316        }
317    
318        public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
319            if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) {
320                // local work directory is configured so we should store file content as files in this local directory
321                return retrieveFileToFileInLocalWorkDirectory(name, exchange);
322            } else {
323                // store file content directory as stream on the body
324                return retrieveFileToStreamInBody(name, exchange);
325            }
326        }
327    
328        private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException {
329            OutputStream os = null;
330            try {
331                os = new ByteArrayOutputStream();
332                GenericFile<ChannelSftp.LsEntry> target =
333                    (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
334                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
335                target.setBody(os);
336                channel.get(name, os);
337                return true;
338            } catch (SftpException e) {
339                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
340            } finally {
341                ObjectHelper.close(os, "retrieve: " + name, LOG);
342            }
343        }
344    
345        private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException {
346            File temp;
347            File local = new File(endpoint.getLocalWorkDirectory());
348            OutputStream os;
349            GenericFile<ChannelSftp.LsEntry> file = 
350                (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
351            ObjectHelper.notNull(file, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
352            try {
353                // use relative filename in local work directory
354                String relativeName = file.getRelativeFilePath();
355    
356                temp = new File(local, relativeName + ".inprogress");
357                local = new File(local, relativeName);
358    
359                // create directory to local work file
360                local.mkdirs();
361    
362                // delete any existing files
363                if (temp.exists()) {
364                    if (!FileUtil.deleteFile(temp)) {
365                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp);
366                    }
367                }
368                if (local.exists()) {
369                    if (!FileUtil.deleteFile(local)) {
370                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local);
371                    }
372                }
373    
374                // create new temp local work file
375                if (!temp.createNewFile()) {
376                    throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp);
377                }
378    
379                // store content as a file in the local work directory in the temp handle
380                os = new FileOutputStream(temp);
381    
382                // set header with the path to the local work file
383                exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath());
384            } catch (Exception e) {
385                throw new GenericFileOperationFailedException("Cannot create new local work file: " + local);
386            }
387    
388    
389            try {
390                // store the java.io.File handle as the body
391                file.setBody(local);
392                channel.get(name, os);
393            } catch (SftpException e) {
394                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
395            } finally {
396                ObjectHelper.close(os, "retrieve: " + name, LOG);
397            }
398    
399            if (LOG.isDebugEnabled()) {
400                LOG.debug("Retrieve file to local work file result: true");
401            }
402    
403            // operation went okay so rename temp to local after we have retrieved the data
404            if (LOG.isTraceEnabled()) {
405                LOG.trace("Renaming local in progress file from: " + temp + " to: " + local);
406            }
407            if (!FileUtil.renameFile(temp, local)) {
408                throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local);
409            }
410    
411            return true;
412        }
413    
414        public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
415            // if an existing file already exists what should we do?
416            if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) {
417                boolean existFile = existsFile(name);
418                if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) {
419                    // ignore but indicate that the file was written
420                    if (LOG.isTraceEnabled()) {
421                        LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it.");
422                    }
423                    return true;
424                } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) {
425                    throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file.");
426                }
427            }
428    
429            InputStream is = null;
430            try {
431                is = ExchangeHelper.getMandatoryInBody(exchange, InputStream.class);
432                if (endpoint.getFileExist() == GenericFileExist.Append) {
433                    channel.put(is, name, ChannelSftp.APPEND);
434                } else {
435                    // override is default
436                    channel.put(is, name);
437                }
438                return true;
439            } catch (SftpException e) {
440                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
441            } catch (InvalidPayloadException e) {
442                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
443            } finally {
444                ObjectHelper.close(is, "store: " + name, LOG);
445            }
446        }
447    
448        public boolean existsFile(String name) throws GenericFileOperationFailedException {
449            // check whether a file already exists
450            String directory = FileUtil.onlyPath(name);
451            if (directory == null) {
452                return false;
453            }
454    
455            String onlyName = FileUtil.stripPath(name);
456            try {
457                Vector files = channel.ls(directory);
458                // can return either null or an empty list depending on FTP servers
459                if (files == null) {
460                    return false;
461                }
462                for (Object file : files) {
463                    ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) file;
464                    if (entry.getFilename().equals(onlyName)) {
465                        return true;
466                    }
467                }
468                return false;
469            } catch (SftpException e) {
470                throw new GenericFileOperationFailedException(e.getMessage(), e);
471            }
472        }
473    
474        public boolean sendNoop() throws GenericFileOperationFailedException {
475            // is not implemented
476            return true;
477        }
478    }