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 // check if we are interrupted so we can break out 109 if (Thread.currentThread().isInterrupted()) { 110 throw new GenericFileOperationFailedException("Interrupted during connecting", new InterruptedException("Interrupted during connecting")); 111 } 112 113 GenericFileOperationFailedException failed = new GenericFileOperationFailedException("Cannot connect to " + configuration.remoteServerInformation(), e); 114 if (LOG.isTraceEnabled()) { 115 LOG.trace("Cannot connect due: " + failed.getMessage()); 116 } 117 attempt++; 118 if (attempt > endpoint.getMaximumReconnectAttempts()) { 119 throw failed; 120 } 121 if (endpoint.getReconnectDelay() > 0) { 122 try { 123 Thread.sleep(endpoint.getReconnectDelay()); 124 } catch (InterruptedException ie) { 125 // we could potentially also be interrupted during sleep 126 Thread.currentThread().interrupt(); 127 throw new GenericFileOperationFailedException("Interrupted during sleeping", ie); 128 } 129 } 130 } 131 } 132 133 return true; 134 } 135 136 protected Session createSession(final RemoteFileConfiguration configuration) throws JSchException { 137 final JSch jsch = new JSch(); 138 JSch.setLogger(new JSchLogger()); 139 140 SftpConfiguration sftpConfig = (SftpConfiguration) configuration; 141 142 if (isNotEmpty(sftpConfig.getPrivateKeyFile())) { 143 LOG.debug("Using private keyfile: " + sftpConfig.getPrivateKeyFile()); 144 if (isNotEmpty(sftpConfig.getPrivateKeyFilePassphrase())) { 145 jsch.addIdentity(sftpConfig.getPrivateKeyFile(), sftpConfig.getPrivateKeyFilePassphrase()); 146 } else { 147 jsch.addIdentity(sftpConfig.getPrivateKeyFile()); 148 } 149 } 150 151 if (isNotEmpty(sftpConfig.getKnownHostsFile())) { 152 LOG.debug("Using knownhosts file: " + sftpConfig.getKnownHostsFile()); 153 jsch.setKnownHosts(sftpConfig.getKnownHostsFile()); 154 } 155 156 final Session session = jsch.getSession(configuration.getUsername(), configuration.getHost(), configuration.getPort()); 157 158 if (isNotEmpty(sftpConfig.getStrictHostKeyChecking())) { 159 LOG.debug("Using StrickHostKeyChecking: " + sftpConfig.getStrictHostKeyChecking()); 160 session.setConfig("StrictHostKeyChecking", sftpConfig.getStrictHostKeyChecking()); 161 } 162 163 // set user information 164 session.setUserInfo(new UserInfo() { 165 public String getPassphrase() { 166 return null; 167 } 168 169 public String getPassword() { 170 return configuration.getPassword(); 171 } 172 173 public boolean promptPassword(String s) { 174 return true; 175 } 176 177 public boolean promptPassphrase(String s) { 178 return true; 179 } 180 181 public boolean promptYesNo(String s) { 182 LOG.warn("Server asks for confirmation (yes|no): " + s + ". Camel will answer no."); 183 // Return 'false' indicating modification of the hosts file is disabled. 184 return false; 185 } 186 187 public void showMessage(String s) { 188 LOG.trace("Message received from Server: " + s); 189 } 190 }); 191 return session; 192 } 193 194 private static final class JSchLogger implements com.jcraft.jsch.Logger { 195 196 public boolean isEnabled(int level) { 197 switch (level) { 198 case FATAL: 199 return LOG.isFatalEnabled(); 200 case ERROR: 201 return LOG.isErrorEnabled(); 202 case WARN: 203 return LOG.isWarnEnabled(); 204 case INFO: 205 return LOG.isInfoEnabled(); 206 default: 207 return LOG.isDebugEnabled(); 208 } 209 } 210 211 public void log(int level, String message) { 212 switch (level) { 213 case FATAL: 214 LOG.fatal("JSCH -> " + message); 215 break; 216 case ERROR: 217 LOG.error("JSCH -> " + message); 218 break; 219 case WARN: 220 LOG.warn("JSCH -> " + message); 221 break; 222 case INFO: 223 LOG.info("JSCH -> " + message); 224 break; 225 default: 226 LOG.debug("JSCH -> " + message); 227 break; 228 } 229 } 230 } 231 232 public boolean isConnected() throws GenericFileOperationFailedException { 233 return session != null && session.isConnected() && channel != null && channel.isConnected(); 234 } 235 236 public void disconnect() throws GenericFileOperationFailedException { 237 if (session != null && session.isConnected()) { 238 session.disconnect(); 239 } 240 if (channel != null && channel.isConnected()) { 241 channel.disconnect(); 242 } 243 } 244 245 public boolean deleteFile(String name) throws GenericFileOperationFailedException { 246 if (LOG.isDebugEnabled()) { 247 LOG.debug("Deleting file: " + name); 248 } 249 try { 250 channel.rm(name); 251 return true; 252 } catch (SftpException e) { 253 throw new GenericFileOperationFailedException("Cannot delete file: " + name, e); 254 } 255 } 256 257 public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { 258 if (LOG.isDebugEnabled()) { 259 LOG.debug("Renaming file: " + from + " to: " + to); 260 } 261 try { 262 channel.rename(from, to); 263 return true; 264 } catch (SftpException e) { 265 throw new GenericFileOperationFailedException("Cannot rename file from: " + from + " to: " + to, e); 266 } 267 } 268 269 public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { 270 if (LOG.isTraceEnabled()) { 271 LOG.trace("buildDirectory(" + directory + "," + absolute + ")"); 272 } 273 // ignore absolute as all dirs are relative with FTP 274 boolean success = false; 275 276 String originalDirectory = getCurrentDirectory(); 277 try { 278 // maybe the full directory already exists 279 try { 280 channel.cd(directory); 281 success = true; 282 } catch (SftpException e) { 283 // ignore, we could not change directory so try to create it instead 284 } 285 286 if (!success) { 287 if (LOG.isDebugEnabled()) { 288 LOG.debug("Trying to build remote directory: " + directory); 289 } 290 291 try { 292 channel.mkdir(directory); 293 success = true; 294 } catch (SftpException e) { 295 // we are here if the server side doesn't create intermediate folders 296 // so create the folder one by one 297 success = buildDirectoryChunks(directory); 298 } 299 } 300 } catch (IOException e) { 301 throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e); 302 } catch (SftpException e) { 303 throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e); 304 } finally { 305 // change back to original directory 306 if (originalDirectory != null) { 307 changeCurrentDirectory(originalDirectory); 308 } 309 } 310 311 return success; 312 } 313 314 private boolean buildDirectoryChunks(String dirName) throws IOException, SftpException { 315 final StringBuilder sb = new StringBuilder(dirName.length()); 316 final String[] dirs = dirName.split("/|\\\\"); 317 318 boolean success = false; 319 for (String dir : dirs) { 320 sb.append(dir).append('/'); 321 String directory = sb.toString(); 322 if (LOG.isTraceEnabled()) { 323 LOG.trace("Trying to build remote directory by chunk: " + directory); 324 } 325 326 // do not try to build root / folder 327 if (!directory.equals("/")) { 328 try { 329 channel.mkdir(directory); 330 success = true; 331 } catch (SftpException e) { 332 // ignore keep trying to create the rest of the path 333 } 334 } 335 } 336 337 return success; 338 } 339 340 public String getCurrentDirectory() throws GenericFileOperationFailedException { 341 if (LOG.isTraceEnabled()) { 342 LOG.trace("getCurrentDirectory()"); 343 } 344 try { 345 return channel.pwd(); 346 } catch (SftpException e) { 347 throw new GenericFileOperationFailedException("Cannot get current directory", e); 348 } 349 } 350 351 public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { 352 if (LOG.isTraceEnabled()) { 353 LOG.trace("changeCurrentDirectory(" + path + ")"); 354 } 355 if (ObjectHelper.isEmpty(path)) { 356 return; 357 } 358 359 // if it starts with the root path then a little special handling for that 360 if (FileUtil.hasLeadingSeparator(path)) { 361 // change to root path 362 doChangeDirectory(path.substring(0, 1)); 363 path = path.substring(1); 364 } 365 366 // split into multiple dirs 367 final String[] dirs = path.split("/|\\\\"); 368 369 if (dirs == null || dirs.length == 0) { 370 // path was just a relative single path 371 doChangeDirectory(path); 372 return; 373 } 374 375 // there are multiple dirs so do this in chunks 376 for (String dir : dirs) { 377 doChangeDirectory(dir); 378 } 379 } 380 381 private void doChangeDirectory(String path) { 382 if (path == null || ".".equals(path) || ObjectHelper.isEmpty(path)) { 383 return; 384 } 385 386 if (LOG.isTraceEnabled()) { 387 LOG.trace("Changing directory: " + path); 388 } 389 try { 390 channel.cd(path); 391 } catch (SftpException e) { 392 throw new GenericFileOperationFailedException("Cannot change directory to: " + path, e); 393 } 394 } 395 396 public void changeToParentDirectory() throws GenericFileOperationFailedException { 397 if (LOG.isTraceEnabled()) { 398 LOG.trace("changeToParentDirectory()"); 399 } 400 String current = getCurrentDirectory(); 401 402 String parent = FileUtil.compactPath(current + "/.."); 403 // must start with absolute 404 if (!parent.startsWith("/")) { 405 parent = "/" + parent; 406 } 407 408 changeCurrentDirectory(parent); 409 } 410 411 public List<ChannelSftp.LsEntry> listFiles() throws GenericFileOperationFailedException { 412 return listFiles("."); 413 } 414 415 public List<ChannelSftp.LsEntry> listFiles(String path) throws GenericFileOperationFailedException { 416 if (LOG.isTraceEnabled()) { 417 LOG.trace("listFiles(" + path + ")"); 418 } 419 if (ObjectHelper.isEmpty(path)) { 420 // list current directory if file path is not given 421 path = "."; 422 } 423 424 try { 425 final List<ChannelSftp.LsEntry> list = new ArrayList<ChannelSftp.LsEntry>(); 426 Vector files = channel.ls(path); 427 // can return either null or an empty list depending on FTP servers 428 if (files != null) { 429 for (Object file : files) { 430 list.add((ChannelSftp.LsEntry) file); 431 } 432 } 433 return list; 434 } catch (SftpException e) { 435 throw new GenericFileOperationFailedException("Cannot list directory: " + path, e); 436 } 437 } 438 439 public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 440 if (LOG.isTraceEnabled()) { 441 LOG.trace("retrieveFile(" + name + ")"); 442 } 443 if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) { 444 // local work directory is configured so we should store file content as files in this local directory 445 return retrieveFileToFileInLocalWorkDirectory(name, exchange); 446 } else { 447 // store file content directory as stream on the body 448 return retrieveFileToStreamInBody(name, exchange); 449 } 450 } 451 452 @SuppressWarnings("unchecked") 453 private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException { 454 OutputStream os = null; 455 try { 456 os = new ByteArrayOutputStream(); 457 GenericFile<ChannelSftp.LsEntry> target = 458 (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 459 ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set"); 460 target.setBody(os); 461 462 // remember current directory 463 String currentDir = getCurrentDirectory(); 464 465 // change directory to path where the file is to be retrieved 466 // (must do this as some FTP servers cannot retrieve using absolute path) 467 String path = FileUtil.onlyPath(name); 468 if (path != null) { 469 changeCurrentDirectory(path); 470 } 471 String onlyName = FileUtil.stripPath(name); 472 473 // use input stream which works with Apache SSHD used for testing 474 InputStream is = channel.get(onlyName); 475 IOHelper.copyAndCloseInput(is, os); 476 477 // change back to current directory 478 changeCurrentDirectory(currentDir); 479 480 return true; 481 } catch (IOException e) { 482 throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e); 483 } catch (SftpException e) { 484 throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e); 485 } finally { 486 IOHelper.close(os, "retrieve: " + name, LOG); 487 } 488 } 489 490 @SuppressWarnings("unchecked") 491 private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException { 492 File temp; 493 File local = new File(endpoint.getLocalWorkDirectory()); 494 OutputStream os; 495 GenericFile<ChannelSftp.LsEntry> file = 496 (GenericFile<ChannelSftp.LsEntry>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 497 ObjectHelper.notNull(file, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set"); 498 try { 499 // use relative filename in local work directory 500 String relativeName = file.getRelativeFilePath(); 501 502 temp = new File(local, relativeName + ".inprogress"); 503 local = new File(local, relativeName); 504 505 // create directory to local work file 506 local.mkdirs(); 507 508 // delete any existing files 509 if (temp.exists()) { 510 if (!FileUtil.deleteFile(temp)) { 511 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp); 512 } 513 } 514 if (local.exists()) { 515 if (!FileUtil.deleteFile(local)) { 516 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local); 517 } 518 } 519 520 // create new temp local work file 521 if (!temp.createNewFile()) { 522 throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp); 523 } 524 525 // store content as a file in the local work directory in the temp handle 526 os = new FileOutputStream(temp); 527 528 // set header with the path to the local work file 529 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath()); 530 } catch (Exception e) { 531 throw new GenericFileOperationFailedException("Cannot create new local work file: " + local); 532 } 533 534 try { 535 // store the java.io.File handle as the body 536 file.setBody(local); 537 538 // remember current directory 539 String currentDir = getCurrentDirectory(); 540 541 // change directory to path where the file is to be retrieved 542 // (must do this as some FTP servers cannot retrieve using absolute path) 543 String path = FileUtil.onlyPath(name); 544 if (path != null) { 545 changeCurrentDirectory(path); 546 } 547 String onlyName = FileUtil.stripPath(name); 548 549 channel.get(onlyName, os); 550 551 // change back to current directory 552 changeCurrentDirectory(currentDir); 553 554 } catch (SftpException e) { 555 if (LOG.isTraceEnabled()) { 556 LOG.trace("Error occurred during retrieving file: " + name + " to local directory. Deleting local work file: " + temp); 557 } 558 // failed to retrieve the file so we need to close streams and delete in progress file 559 // must close stream before deleting file 560 IOHelper.close(os, "retrieve: " + name, LOG); 561 boolean deleted = FileUtil.deleteFile(temp); 562 if (!deleted) { 563 LOG.warn("Error occurred during retrieving file: " + name + " to local directory. Cannot delete local work file: " + temp); 564 } 565 throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e); 566 } finally { 567 IOHelper.close(os, "retrieve: " + name, LOG); 568 } 569 570 if (LOG.isDebugEnabled()) { 571 LOG.debug("Retrieve file to local work file result: true"); 572 } 573 574 // operation went okay so rename temp to local after we have retrieved the data 575 if (LOG.isTraceEnabled()) { 576 LOG.trace("Renaming local in progress file from: " + temp + " to: " + local); 577 } 578 if (!FileUtil.renameFile(temp, local)) { 579 throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local); 580 } 581 582 return true; 583 } 584 585 public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 586 if (LOG.isTraceEnabled()) { 587 LOG.trace("storeFile(" + name + ")"); 588 } 589 590 // if an existing file already exists what should we do? 591 if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) { 592 boolean existFile = existsFile(name); 593 if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) { 594 // ignore but indicate that the file was written 595 if (LOG.isTraceEnabled()) { 596 LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it."); 597 } 598 return true; 599 } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) { 600 throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file."); 601 } 602 } 603 604 InputStream is = null; 605 try { 606 is = ExchangeHelper.getMandatoryInBody(exchange, InputStream.class); 607 if (endpoint.getFileExist() == GenericFileExist.Append) { 608 channel.put(is, name, ChannelSftp.APPEND); 609 } else { 610 // override is default 611 channel.put(is, name); 612 } 613 return true; 614 } catch (SftpException e) { 615 throw new GenericFileOperationFailedException("Cannot store file: " + name, e); 616 } catch (InvalidPayloadException e) { 617 throw new GenericFileOperationFailedException("Cannot store file: " + name, e); 618 } finally { 619 IOHelper.close(is, "store: " + name, LOG); 620 } 621 } 622 623 public boolean existsFile(String name) throws GenericFileOperationFailedException { 624 if (LOG.isTraceEnabled()) { 625 LOG.trace("existsFile(" + name + ")"); 626 } 627 628 // check whether a file already exists 629 String directory = FileUtil.onlyPath(name); 630 if (directory == null) { 631 // assume current dir if no path could be extracted 632 directory = ""; 633 } 634 String onlyName = FileUtil.stripPath(name); 635 636 try { 637 Vector files = channel.ls(directory); 638 // can return either null or an empty list depending on FTP servers 639 if (files == null) { 640 return false; 641 } 642 for (Object file : files) { 643 ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) file; 644 if (entry.getFilename().equals(onlyName)) { 645 return true; 646 } 647 } 648 return false; 649 } catch (SftpException e) { 650 // or an exception can be thrown with id 2 which means file does not exists 651 if (ChannelSftp.SSH_FX_NO_SUCH_FILE == e.id) { 652 return false; 653 } 654 // otherwise its a more serious error so rethrow 655 throw new GenericFileOperationFailedException(e.getMessage(), e); 656 } 657 } 658 659 public boolean sendNoop() throws GenericFileOperationFailedException { 660 // is not implemented 661 return true; 662 } 663 664 public boolean sendSiteCommand(String command) throws GenericFileOperationFailedException { 665 // is not implemented 666 return true; 667 } 668 }