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