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 }