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