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        @SuppressWarnings("unchecked")
329        private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException {
330            OutputStream os = null;
331            try {
332                os = new ByteArrayOutputStream();
333                GenericFile<ChannelSftp.LsEntry> target =
334                    (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
335                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
336                target.setBody(os);
337                channel.get(name, os);
338                return true;
339            } catch (SftpException e) {
340                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
341            } finally {
342                ObjectHelper.close(os, "retrieve: " + name, LOG);
343            }
344        }
345    
346        @SuppressWarnings("unchecked")
347        private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException {
348            File temp;
349            File local = new File(endpoint.getLocalWorkDirectory());
350            OutputStream os;
351            GenericFile<ChannelSftp.LsEntry> file = 
352                (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
353            ObjectHelper.notNull(file, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
354            try {
355                // use relative filename in local work directory
356                String relativeName = file.getRelativeFilePath();
357    
358                temp = new File(local, relativeName + ".inprogress");
359                local = new File(local, relativeName);
360    
361                // create directory to local work file
362                local.mkdirs();
363    
364                // delete any existing files
365                if (temp.exists()) {
366                    if (!FileUtil.deleteFile(temp)) {
367                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp);
368                    }
369                }
370                if (local.exists()) {
371                    if (!FileUtil.deleteFile(local)) {
372                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local);
373                    }
374                }
375    
376                // create new temp local work file
377                if (!temp.createNewFile()) {
378                    throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp);
379                }
380    
381                // store content as a file in the local work directory in the temp handle
382                os = new FileOutputStream(temp);
383    
384                // set header with the path to the local work file
385                exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath());
386            } catch (Exception e) {
387                throw new GenericFileOperationFailedException("Cannot create new local work file: " + local);
388            }
389    
390    
391            try {
392                // store the java.io.File handle as the body
393                file.setBody(local);
394                channel.get(name, os);
395            } catch (SftpException e) {
396                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
397            } finally {
398                ObjectHelper.close(os, "retrieve: " + name, LOG);
399            }
400    
401            if (LOG.isDebugEnabled()) {
402                LOG.debug("Retrieve file to local work file result: true");
403            }
404    
405            // operation went okay so rename temp to local after we have retrieved the data
406            if (LOG.isTraceEnabled()) {
407                LOG.trace("Renaming local in progress file from: " + temp + " to: " + local);
408            }
409            if (!FileUtil.renameFile(temp, local)) {
410                throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local);
411            }
412    
413            return true;
414        }
415    
416        public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
417            // if an existing file already exists what should we do?
418            if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) {
419                boolean existFile = existsFile(name);
420                if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) {
421                    // ignore but indicate that the file was written
422                    if (LOG.isTraceEnabled()) {
423                        LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it.");
424                    }
425                    return true;
426                } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) {
427                    throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file.");
428                }
429            }
430    
431            InputStream is = null;
432            try {
433                is = ExchangeHelper.getMandatoryInBody(exchange, InputStream.class);
434                if (endpoint.getFileExist() == GenericFileExist.Append) {
435                    channel.put(is, name, ChannelSftp.APPEND);
436                } else {
437                    // override is default
438                    channel.put(is, name);
439                }
440                return true;
441            } catch (SftpException e) {
442                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
443            } catch (InvalidPayloadException e) {
444                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
445            } finally {
446                ObjectHelper.close(is, "store: " + name, LOG);
447            }
448        }
449    
450        public boolean existsFile(String name) throws GenericFileOperationFailedException {
451            // check whether a file already exists
452            String directory = FileUtil.onlyPath(name);
453            if (directory == null) {
454                return false;
455            }
456    
457            String onlyName = FileUtil.stripPath(name);
458            try {
459                Vector files = channel.ls(directory);
460                // can return either null or an empty list depending on FTP servers
461                if (files == null) {
462                    return false;
463                }
464                for (Object file : files) {
465                    ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) file;
466                    if (entry.getFilename().equals(onlyName)) {
467                        return true;
468                    }
469                }
470                return false;
471            } catch (SftpException e) {
472                throw new GenericFileOperationFailedException(e.getMessage(), e);
473            }
474        }
475    
476        public boolean sendNoop() throws GenericFileOperationFailedException {
477            // is not implemented
478            return true;
479        }
480    }