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 }