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.GenericFile; 039 import org.apache.camel.component.file.GenericFileEndpoint; 040 import org.apache.camel.component.file.GenericFileExchange; 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 * SFTP remote file operations 052 */ 053 public class SftpOperations implements RemoteFileOperations<ChannelSftp.LsEntry> { 054 private static final transient Log LOG = LogFactory.getLog(SftpOperations.class); 055 private GenericFileEndpoint endpoint; 056 private ChannelSftp channel; 057 private Session session; 058 059 public void setEndpoint(GenericFileEndpoint endpoint) { 060 this.endpoint = endpoint; 061 } 062 063 public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException { 064 try { 065 if (isConnected()) { 066 // already connected 067 return true; 068 } 069 if (channel == null || !channel.isConnected()) { 070 if (session == null || !session.isConnected()) { 071 LOG.trace("Session isn't connected, trying to recreate and connect."); 072 session = createSession(configuration); 073 session.connect(); 074 } 075 LOG.trace("Channel isn't connected, trying to recreate and connect."); 076 channel = (ChannelSftp) session.openChannel("sftp"); 077 channel.connect(); 078 LOG.info("Connected to " + configuration.remoteServerInformation()); 079 } 080 081 return true; 082 083 } catch (JSchException e) { 084 throw new GenericFileOperationFailedException("Cannot connect to " + configuration.remoteServerInformation(), e); 085 } 086 } 087 088 protected Session createSession(final RemoteFileConfiguration configuration) throws JSchException { 089 final JSch jsch = new JSch(); 090 091 SftpConfiguration sftpConfig = (SftpConfiguration) configuration; 092 093 if (isNotEmpty(sftpConfig.getPrivateKeyFile())) { 094 LOG.debug("Using private keyfile: " + sftpConfig.getPrivateKeyFile()); 095 if (isNotEmpty(sftpConfig.getPrivateKeyFilePassphrase())) { 096 jsch.addIdentity(sftpConfig.getPrivateKeyFile(), sftpConfig.getPrivateKeyFilePassphrase()); 097 } else { 098 jsch.addIdentity(sftpConfig.getPrivateKeyFile()); 099 } 100 } 101 102 if (isNotEmpty(sftpConfig.getKnownHostsFile())) { 103 LOG.debug("Using knownhosts file: " + sftpConfig.getKnownHostsFile()); 104 jsch.setKnownHosts(sftpConfig.getKnownHostsFile()); 105 } 106 107 final Session session = jsch.getSession(configuration.getUsername(), configuration.getHost(), configuration.getPort()); 108 session.setUserInfo(new UserInfo() { 109 public String getPassphrase() { 110 return null; 111 } 112 113 public String getPassword() { 114 return configuration.getPassword(); 115 } 116 117 public boolean promptPassword(String s) { 118 return true; 119 } 120 121 public boolean promptPassphrase(String s) { 122 return true; 123 } 124 125 public boolean promptYesNo(String s) { 126 LOG.error(s); 127 // Return 'false' indicating modification of the hosts file is disabled. 128 return false; 129 } 130 131 public void showMessage(String s) { 132 } 133 }); 134 return session; 135 } 136 137 public boolean isConnected() throws GenericFileOperationFailedException { 138 return session != null && session.isConnected() && channel != null && channel.isConnected(); 139 } 140 141 public void disconnect() throws GenericFileOperationFailedException { 142 if (session != null && session.isConnected()) { 143 session.disconnect(); 144 } 145 if (channel != null && channel.isConnected()) { 146 channel.disconnect(); 147 } 148 } 149 150 public boolean deleteFile(String name) throws GenericFileOperationFailedException { 151 if (LOG.isDebugEnabled()) { 152 LOG.debug("Deleteing file: " + name); 153 } 154 try { 155 channel.rm(name); 156 return true; 157 } catch (SftpException e) { 158 throw new GenericFileOperationFailedException("Cannot delete file: " + name, e); 159 } 160 } 161 162 public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { 163 if (LOG.isDebugEnabled()) { 164 LOG.debug("Renaming file: " + from + " to: " + to); 165 } 166 try { 167 channel.rename(from, to); 168 return true; 169 } catch (SftpException e) { 170 throw new GenericFileOperationFailedException("Cannot rename file from: " + from + " to: " + to, e); 171 } 172 } 173 174 public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { 175 // ignore absolute as all dirs are relative with FTP 176 boolean success = false; 177 178 String originalDirectory = getCurrentDirectory(); 179 try { 180 // maybe the full directory already exsits 181 try { 182 channel.cd(directory); 183 success = true; 184 } catch (SftpException e) { 185 // ignore, we could not change directory so try to create it instead 186 } 187 188 if (!success) { 189 if (LOG.isDebugEnabled()) { 190 LOG.debug("Trying to build remote directory: " + directory); 191 } 192 193 try { 194 channel.mkdir(directory); 195 success = true; 196 } catch (SftpException e) { 197 // we are here if the server side doesn't create intermediate folders 198 // so create the folder one by one 199 success = buildDirectoryChunks(directory); 200 } 201 } 202 } catch (IOException e) { 203 throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e); 204 } catch (SftpException e) { 205 throw new GenericFileOperationFailedException("Cannot build directory: " + directory, e); 206 } finally { 207 // change back to original directory 208 if (originalDirectory != null) { 209 changeCurrentDirectory(originalDirectory); 210 } 211 } 212 213 return success; 214 } 215 216 private boolean buildDirectoryChunks(String dirName) throws IOException, SftpException { 217 final StringBuilder sb = new StringBuilder(dirName.length()); 218 final String[] dirs = dirName.split("/|\\\\"); 219 220 boolean success = false; 221 for (String dir : dirs) { 222 sb.append(dir).append('/'); 223 String directory = sb.toString(); 224 if (LOG.isTraceEnabled()) { 225 LOG.trace("Trying to build remote directory by chunk: " + directory); 226 } 227 228 // do not try to build root / folder 229 if (!directory.equals("/")) { 230 try { 231 channel.mkdir(directory); 232 success = true; 233 } catch (SftpException e) { 234 // ignore keep trying to create the rest of the path 235 } 236 } 237 } 238 239 return success; 240 } 241 242 public String getCurrentDirectory() throws GenericFileOperationFailedException { 243 try { 244 return channel.pwd(); 245 } catch (SftpException e) { 246 throw new GenericFileOperationFailedException("Cannot get current directory", e); 247 } 248 } 249 250 public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { 251 try { 252 channel.cd(path); 253 } catch (SftpException e) { 254 throw new GenericFileOperationFailedException("Cannot change current directory to: " + path, e); 255 } 256 } 257 258 public List<ChannelSftp.LsEntry> listFiles() throws GenericFileOperationFailedException { 259 return listFiles("."); 260 } 261 262 public List<ChannelSftp.LsEntry> listFiles(String path) throws GenericFileOperationFailedException { 263 if (ObjectHelper.isEmpty(path)) { 264 // list current dirctory if file path is not given 265 path = "."; 266 } 267 268 try { 269 final List<ChannelSftp.LsEntry> list = new ArrayList<ChannelSftp.LsEntry>(); 270 Vector files = channel.ls(path); 271 for (Object file : files) { 272 list.add((ChannelSftp.LsEntry)file); 273 } 274 return list; 275 } catch (SftpException e) { 276 throw new GenericFileOperationFailedException("Cannot list directory: " + path, e); 277 } 278 } 279 280 public boolean retrieveFile(String name, GenericFileExchange<ChannelSftp.LsEntry> exchange) throws GenericFileOperationFailedException { 281 if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) { 282 // local work directory is configured so we should store file content as files in this local directory 283 return retrieveFileToFileInLocalWorkDirectory(name, exchange); 284 } else { 285 // store file content directory as stream on the body 286 return retrieveFileToStreamInBody(name, exchange); 287 } 288 } 289 290 private boolean retrieveFileToStreamInBody(String name, GenericFileExchange<ChannelSftp.LsEntry> exchange) throws GenericFileOperationFailedException { 291 try { 292 GenericFile<ChannelSftp.LsEntry> target = exchange.getGenericFile(); 293 OutputStream os = new ByteArrayOutputStream(); 294 target.setBody(os); 295 channel.get(name, os); 296 return true; 297 } catch (SftpException e) { 298 throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e); 299 } 300 } 301 302 private boolean retrieveFileToFileInLocalWorkDirectory(String name, GenericFileExchange<ChannelSftp.LsEntry> exchange) throws GenericFileOperationFailedException { 303 File temp; 304 File local = new File(endpoint.getLocalWorkDirectory()); 305 OutputStream os; 306 try { 307 // use relative filename in local work directory 308 String relativeName = exchange.getGenericFile().getRelativeFilePath(); 309 310 temp = new File(local, relativeName + ".inprogress"); 311 local = new File(local, relativeName); 312 313 // create directory to local work file 314 local.mkdirs(); 315 316 // delete any existing files 317 if (temp.exists()) { 318 if (!temp.delete()) { 319 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp); 320 } 321 } 322 if (local.exists()) { 323 if (!local.delete()) { 324 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local); 325 } 326 } 327 328 // create new temp local work file 329 if (!temp.createNewFile()) { 330 throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp); 331 } 332 333 // store content as a file in the local work directory in the temp handle 334 os = new FileOutputStream(temp); 335 336 // set header with the path to the local work file 337 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath()); 338 339 340 } catch (Exception e) { 341 throw new GenericFileOperationFailedException("Cannot create new local work file: " + local); 342 } 343 344 try { 345 GenericFile<ChannelSftp.LsEntry> target = exchange.getGenericFile(); 346 // store the java.io.File handle as the body 347 target.setBody(local); 348 channel.get(name, os); 349 350 // rename temp to local after we have retrieved the data 351 if (!temp.renameTo(local)) { 352 throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local); 353 } 354 } catch (SftpException e) { 355 throw new GenericFileOperationFailedException("Cannot retrieve file: " + name, e); 356 } finally { 357 ObjectHelper.close(os, "retrieve: " + name, LOG); 358 } 359 360 return true; 361 } 362 363 public boolean storeFile(String name, GenericFileExchange<ChannelSftp.LsEntry> exchange) throws GenericFileOperationFailedException { 364 // if an existing file already exsists what should we do? 365 if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) { 366 boolean existFile = existFile(name); 367 if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) { 368 // ignore but indicate that the file was written 369 if (LOG.isTraceEnabled()) { 370 LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it."); 371 } 372 return true; 373 } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) { 374 throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file."); 375 } 376 } 377 378 try { 379 InputStream in = ExchangeHelper.getMandatoryInBody(exchange, InputStream.class); 380 if (endpoint.getFileExist() == GenericFileExist.Append) { 381 channel.put(in, name, ChannelSftp.APPEND); 382 } else { 383 // override is default 384 channel.put(in, name); 385 } 386 return true; 387 } catch (SftpException e) { 388 throw new GenericFileOperationFailedException("Cannot store file: " + name, e); 389 } catch (InvalidPayloadException e) { 390 throw new GenericFileOperationFailedException("Cannot store file: " + name, e); 391 } 392 } 393 394 private boolean existFile(String name) { 395 // check whether a file already exists 396 String directory = FileUtil.onlyPath(name); 397 if (directory == null) { 398 return false; 399 } 400 401 String onlyName = FileUtil.stripPath(name); 402 try { 403 Vector files = channel.ls(directory); 404 for (Object file : files) { 405 ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) file; 406 if (entry.getFilename().equals(onlyName)) { 407 return true; 408 } 409 } 410 return false; 411 } catch (SftpException e) { 412 throw new GenericFileOperationFailedException(e.getMessage(), e); 413 } 414 } 415 416 }