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            session.setUserInfo(new UserInfo() {
136                public String getPassphrase() {
137                    return null;
138                }
139    
140                public String getPassword() {
141                    return configuration.getPassword();
142                }
143    
144                public boolean promptPassword(String s) {
145                    return true;
146                }
147    
148                public boolean promptPassphrase(String s) {
149                    return true;
150                }
151    
152                public boolean promptYesNo(String s) {
153                    LOG.error(s);
154                    // Return 'false' indicating modification of the hosts file is disabled.
155                    return false;
156                }
157    
158                public void showMessage(String s) {
159                }
160            });
161            return session;
162        }
163    
164        public boolean isConnected() throws GenericFileOperationFailedException {
165            return session != null && session.isConnected() && channel != null && channel.isConnected();
166        }
167    
168        public void disconnect() throws GenericFileOperationFailedException {
169            if (session != null && session.isConnected()) {
170                session.disconnect();
171            }
172            if (channel != null && channel.isConnected()) {
173                channel.disconnect();
174            }
175        }
176    
177        public boolean deleteFile(String name) throws GenericFileOperationFailedException {
178            if (LOG.isDebugEnabled()) {
179                LOG.debug("Deleting file: " + name);
180            }
181            try {
182                channel.rm(name);
183                return true;
184            } catch (SftpException e) {
185                throw new GenericFileOperationFailedException("Cannot delete file: " + name, e);
186            }
187        }
188    
189        public boolean renameFile(String from, String to) throws GenericFileOperationFailedException {
190            if (LOG.isDebugEnabled()) {
191                LOG.debug("Renaming file: " + from + " to: " + to);
192            }
193            try {
194                channel.rename(from, to);
195                return true;
196            } catch (SftpException e) {
197                throw new GenericFileOperationFailedException("Cannot rename file from: " + from + " to: " + to, e);
198            }
199        }
200    
201        public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException {
202            // ignore absolute as all dirs are relative with FTP
203            boolean success = false;
204    
205            String originalDirectory = getCurrentDirectory();
206            try {
207                // maybe the full directory already exists
208                try {
209                    channel.cd(directory);
210                    success = true;
211                } catch (SftpException e) {
212                    // ignore, we could not change directory so try to create it instead
213                }
214    
215                if (!success) {
216                    if (LOG.isDebugEnabled()) {
217                        LOG.debug("Trying to build remote directory: " + directory);
218                    }
219    
220                    try {
221                        channel.mkdir(directory);
222                        success = true;
223                    } catch (SftpException e) {
224                        // we are here if the server side doesn't create intermediate folders
225                        // so create the folder one by one
226                        success = buildDirectoryChunks(directory);
227                    }
228                }
229            } catch (IOException e) {
230                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
231            } catch (SftpException e) {
232                throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e);
233            } finally {
234                // change back to original directory
235                if (originalDirectory != null) {
236                    changeCurrentDirectory(originalDirectory);
237                }
238            }
239    
240            return success;
241        }
242    
243        private boolean buildDirectoryChunks(String dirName) throws IOException, SftpException {
244            final StringBuilder sb = new StringBuilder(dirName.length());
245            final String[] dirs = dirName.split("/|\\\\");
246    
247            boolean success = false;
248            for (String dir : dirs) {
249                sb.append(dir).append('/');
250                String directory = sb.toString();
251                if (LOG.isTraceEnabled()) {
252                    LOG.trace("Trying to build remote directory by chunk: " + directory);
253                }
254    
255                // do not try to build root / folder
256                if (!directory.equals("/")) {
257                    try {
258                        channel.mkdir(directory);
259                        success = true;
260                    } catch (SftpException e) {
261                        // ignore keep trying to create the rest of the path
262                    }
263                }
264            }
265    
266            return success;
267        }
268    
269        public String getCurrentDirectory() throws GenericFileOperationFailedException {
270            try {
271                return channel.pwd();
272            } catch (SftpException e) {
273                throw new GenericFileOperationFailedException("Cannot get current directory", e);
274            }
275        }
276    
277        public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException {
278            try {
279                channel.cd(path);
280            } catch (SftpException e) {
281                throw new GenericFileOperationFailedException("Cannot change current directory to: " + path, e);
282            }
283        }
284    
285        public List<ChannelSftp.LsEntry> listFiles() throws GenericFileOperationFailedException {
286            return listFiles(".");
287        }
288    
289        public List<ChannelSftp.LsEntry> listFiles(String path) throws GenericFileOperationFailedException {
290            if (ObjectHelper.isEmpty(path)) {
291                // list current directory if file path is not given
292                path = ".";
293            }
294    
295            try {
296                final List<ChannelSftp.LsEntry> list = new ArrayList<ChannelSftp.LsEntry>();
297                Vector files = channel.ls(path);
298                // can return either null or an empty list depending on FTP servers
299                if (files != null) {
300                    for (Object file : files) {
301                        list.add((ChannelSftp.LsEntry)file);
302                    }
303                }
304                return list;
305            } catch (SftpException e) {
306                throw new GenericFileOperationFailedException("Cannot list directory: " + path, e);
307            }
308        }
309    
310        public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
311            if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) {
312                // local work directory is configured so we should store file content as files in this local directory
313                return retrieveFileToFileInLocalWorkDirectory(name, exchange);
314            } else {
315                // store file content directory as stream on the body
316                return retrieveFileToStreamInBody(name, exchange);
317            }
318        }
319    
320        private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException {
321            OutputStream os = null;
322            try {
323                os = new ByteArrayOutputStream();
324                GenericFile<ChannelSftp.LsEntry> target =
325                    (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
326                ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
327                target.setBody(os);
328                channel.get(name, os);
329                return true;
330            } catch (SftpException e) {
331                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
332            } finally {
333                ObjectHelper.close(os, "retrieve: " + name, LOG);
334            }
335        }
336    
337        private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException {
338            File temp;
339            File local = new File(endpoint.getLocalWorkDirectory());
340            OutputStream os;
341            GenericFile<ChannelSftp.LsEntry> file = 
342                (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE);
343            ObjectHelper.notNull(file, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set");
344            try {
345                // use relative filename in local work directory
346                String relativeName = file.getRelativeFilePath();
347    
348                temp = new File(local, relativeName + ".inprogress");
349                local = new File(local, relativeName);
350    
351                // create directory to local work file
352                local.mkdirs();
353    
354                // delete any existing files
355                if (temp.exists()) {
356                    if (!FileUtil.deleteFile(temp)) {
357                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp);
358                    }
359                }
360                if (local.exists()) {
361                    if (!FileUtil.deleteFile(local)) {
362                        throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local);
363                    }
364                }
365    
366                // create new temp local work file
367                if (!temp.createNewFile()) {
368                    throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp);
369                }
370    
371                // store content as a file in the local work directory in the temp handle
372                os = new FileOutputStream(temp);
373    
374                // set header with the path to the local work file
375                exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath());
376            } catch (Exception e) {
377                throw new GenericFileOperationFailedException("Cannot create new local work file: " + local);
378            }
379    
380    
381            try {
382                // store the java.io.File handle as the body
383                file.setBody(local);
384                channel.get(name, os);
385            } catch (SftpException e) {
386                throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e);
387            } finally {
388                ObjectHelper.close(os, "retrieve: " + name, LOG);
389            }
390    
391            if (LOG.isDebugEnabled()) {
392                LOG.debug("Retrieve file to local work file result: true");
393            }
394    
395            // operation went okay so rename temp to local after we have retrieved the data
396            if (LOG.isTraceEnabled()) {
397                LOG.trace("Renaming local in progress file from: " + temp + " to: " + local);
398            }
399            if (!FileUtil.renameFile(temp, local)) {
400                throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local);
401            }
402    
403            return true;
404        }
405    
406        public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException {
407            // if an existing file already exists what should we do?
408            if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) {
409                boolean existFile = existsFile(name);
410                if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) {
411                    // ignore but indicate that the file was written
412                    if (LOG.isTraceEnabled()) {
413                        LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it.");
414                    }
415                    return true;
416                } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) {
417                    throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file.");
418                }
419            }
420    
421            InputStream is = null;
422            try {
423                is = ExchangeHelper.getMandatoryInBody(exchange, InputStream.class);
424                if (endpoint.getFileExist() == GenericFileExist.Append) {
425                    channel.put(is, name, ChannelSftp.APPEND);
426                } else {
427                    // override is default
428                    channel.put(is, name);
429                }
430                return true;
431            } catch (SftpException e) {
432                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
433            } catch (InvalidPayloadException e) {
434                throw new GenericFileOperationFailedException("Cannot store file: " + name, e);
435            } finally {
436                ObjectHelper.close(is, "store: " + name, LOG);
437            }
438        }
439    
440        public boolean existsFile(String name) throws GenericFileOperationFailedException {
441            // check whether a file already exists
442            String directory = FileUtil.onlyPath(name);
443            if (directory == null) {
444                return false;
445            }
446    
447            String onlyName = FileUtil.stripPath(name);
448            try {
449                Vector files = channel.ls(directory);
450                // can return either null or an empty list depending on FTP servers
451                if (files == null) {
452                    return false;
453                }
454                for (Object file : files) {
455                    ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) file;
456                    if (entry.getFilename().equals(onlyName)) {
457                        return true;
458                    }
459                }
460                return false;
461            } catch (SftpException e) {
462                throw new GenericFileOperationFailedException(e.getMessage(), e);
463            }
464        }
465    
466        public boolean sendNoop() throws GenericFileOperationFailedException {
467            // is not implemented
468            return true;
469        }
470    }