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.Arrays; 027 import java.util.List; 028 029 import org.apache.camel.Exchange; 030 import org.apache.camel.InvalidPayloadException; 031 import org.apache.camel.component.file.FileComponent; 032 import org.apache.camel.component.file.GenericFile; 033 import org.apache.camel.component.file.GenericFileEndpoint; 034 import org.apache.camel.component.file.GenericFileExist; 035 import org.apache.camel.component.file.GenericFileOperationFailedException; 036 import org.apache.camel.util.FileUtil; 037 import org.apache.camel.util.IOHelper; 038 import org.apache.camel.util.ObjectHelper; 039 import org.apache.commons.logging.Log; 040 import org.apache.commons.logging.LogFactory; 041 import org.apache.commons.net.ftp.FTPClient; 042 import org.apache.commons.net.ftp.FTPClientConfig; 043 import org.apache.commons.net.ftp.FTPFile; 044 import org.apache.commons.net.ftp.FTPReply; 045 046 /** 047 * FTP remote file operations 048 */ 049 public class FtpOperations implements RemoteFileOperations<FTPFile> { 050 051 protected final transient Log log = LogFactory.getLog(getClass()); 052 protected final FTPClient client; 053 protected final FTPClientConfig clientConfig; 054 protected RemoteFileEndpoint<FTPFile> endpoint; 055 056 public FtpOperations(FTPClient client, FTPClientConfig clientConfig) { 057 this.client = client; 058 this.clientConfig = clientConfig; 059 } 060 061 public void setEndpoint(GenericFileEndpoint<FTPFile> endpoint) { 062 this.endpoint = (RemoteFileEndpoint<FTPFile>) endpoint; 063 } 064 065 public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException { 066 if (log.isTraceEnabled()) { 067 log.trace("Connecting using FTPClient: " + client); 068 } 069 070 String host = configuration.getHost(); 071 int port = configuration.getPort(); 072 String username = configuration.getUsername(); 073 074 if (clientConfig != null) { 075 log.trace("Configuring FTPClient with config: " + clientConfig); 076 client.configure(clientConfig); 077 } 078 079 if (log.isTraceEnabled()) { 080 log.trace("Connecting to " + configuration.remoteServerInformation() + " using connection timeout: " + client.getConnectTimeout()); 081 } 082 083 boolean connected = false; 084 int attempt = 0; 085 086 while (!connected) { 087 try { 088 if (log.isTraceEnabled() && attempt > 0) { 089 log.trace("Reconnect attempt #" + attempt + " connecting to + " + configuration.remoteServerInformation()); 090 } 091 client.connect(host, port); 092 // must check reply code if we are connected 093 int reply = client.getReplyCode(); 094 095 if (FTPReply.isPositiveCompletion(reply)) { 096 // yes we could connect 097 connected = true; 098 } else { 099 // throw an exception to force the retry logic in the catch exception block 100 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), "Server refused connection"); 101 } 102 } catch (Exception e) { 103 GenericFileOperationFailedException failed; 104 if (e instanceof GenericFileOperationFailedException) { 105 failed = (GenericFileOperationFailedException) e; 106 } else { 107 failed = new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 108 } 109 110 if (log.isTraceEnabled()) { 111 log.trace("Cannot connect due: " + failed.getMessage()); 112 } 113 attempt++; 114 if (attempt > endpoint.getMaximumReconnectAttempts()) { 115 throw failed; 116 } 117 if (endpoint.getReconnectDelay() > 0) { 118 try { 119 Thread.sleep(endpoint.getReconnectDelay()); 120 } catch (InterruptedException ie) { 121 // ignore 122 } 123 } 124 } 125 } 126 127 // must enter passive mode directly after connect 128 if (configuration.isPassiveMode()) { 129 log.trace("Using passive mode connections"); 130 client.enterLocalPassiveMode(); 131 } 132 133 // must set soTimeout after connect 134 if (endpoint instanceof FtpEndpoint) { 135 FtpEndpoint ftpEndpoint = (FtpEndpoint) endpoint; 136 if (ftpEndpoint.getSoTimeout() > 0) { 137 log.trace("Using SoTimeout=" + ftpEndpoint.getSoTimeout()); 138 try { 139 client.setSoTimeout(ftpEndpoint.getSoTimeout()); 140 } catch (IOException e) { 141 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 142 } 143 } 144 } 145 146 try { 147 boolean login; 148 if (username != null) { 149 if (log.isTraceEnabled()) { 150 log.trace("Attempting to login user: " + username + " using password: " + configuration.getPassword()); 151 } 152 login = client.login(username, configuration.getPassword()); 153 } else { 154 log.trace("Attempting to login anonymous"); 155 login = client.login("anonymous", null); 156 } 157 if (log.isTraceEnabled()) { 158 log.trace("User " + (username != null ? username : "anonymous") + " logged in: " + login); 159 } 160 if (!login) { 161 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString()); 162 } 163 client.setFileType(configuration.isBinary() ? FTPClient.BINARY_FILE_TYPE : FTPClient.ASCII_FILE_TYPE); 164 } catch (IOException e) { 165 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 166 } 167 168 return true; 169 } 170 171 public boolean isConnected() throws GenericFileOperationFailedException { 172 return client.isConnected(); 173 } 174 175 public void disconnect() throws GenericFileOperationFailedException { 176 // logout before disconnecting 177 try { 178 client.logout(); 179 } catch (IOException e) { 180 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 181 } finally { 182 try { 183 client.disconnect(); 184 } catch (IOException e) { 185 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 186 } 187 } 188 } 189 190 public boolean deleteFile(String name) throws GenericFileOperationFailedException { 191 if (log.isDebugEnabled()) { 192 log.debug("Deleting file: " + name); 193 } 194 try { 195 return this.client.deleteFile(name); 196 } catch (IOException e) { 197 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 198 } 199 } 200 201 public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { 202 if (log.isDebugEnabled()) { 203 log.debug("Renaming file: " + from + " to: " + to); 204 } 205 try { 206 return client.rename(from, to); 207 } catch (IOException e) { 208 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 209 } 210 } 211 212 public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { 213 if (log.isTraceEnabled()) { 214 log.trace("Building directory: " + directory); 215 } 216 try { 217 String originalDirectory = client.printWorkingDirectory(); 218 219 boolean success; 220 try { 221 // maybe the full directory already exists 222 success = client.changeWorkingDirectory(directory); 223 if (!success) { 224 if (log.isTraceEnabled()) { 225 log.trace("Trying to build remote directory: " + directory); 226 } 227 success = client.makeDirectory(directory); 228 if (!success) { 229 // we are here if the server side doesn't create intermediate folders so create the folder one by one 230 success = buildDirectoryChunks(directory); 231 } 232 } 233 234 return success; 235 } finally { 236 // change back to original directory 237 if (originalDirectory != null) { 238 client.changeWorkingDirectory(originalDirectory); 239 } 240 } 241 } catch (IOException e) { 242 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 243 } 244 } 245 246 public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 247 if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) { 248 // local work directory is configured so we should store file content as files in this local directory 249 return retrieveFileToFileInLocalWorkDirectory(name, exchange); 250 } else { 251 // store file content directory as stream on the body 252 return retrieveFileToStreamInBody(name, exchange); 253 } 254 } 255 256 @SuppressWarnings("unchecked") 257 private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException { 258 OutputStream os = null; 259 try { 260 os = new ByteArrayOutputStream(); 261 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 262 ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set"); 263 target.setBody(os); 264 return client.retrieveFile(name, os); 265 } catch (IOException e) { 266 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 267 } finally { 268 IOHelper.close(os, "retrieve: " + name, log); 269 } 270 } 271 272 @SuppressWarnings("unchecked") 273 private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException { 274 File temp; 275 File local = new File(FileUtil.normalizePath(endpoint.getLocalWorkDirectory())); 276 OutputStream os; 277 try { 278 // use relative filename in local work directory 279 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 280 ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set"); 281 String relativeName = target.getRelativeFilePath(); 282 283 temp = new File(local, relativeName + ".inprogress"); 284 local = new File(local, relativeName); 285 286 // create directory to local work file 287 local.mkdirs(); 288 289 // delete any existing files 290 if (temp.exists()) { 291 if (!FileUtil.deleteFile(temp)) { 292 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp); 293 } 294 } 295 if (local.exists()) { 296 if (!FileUtil.deleteFile(local)) { 297 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local); 298 } 299 } 300 301 // create new temp local work file 302 if (!temp.createNewFile()) { 303 throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp); 304 } 305 306 // store content as a file in the local work directory in the temp handle 307 os = new FileOutputStream(temp); 308 309 // set header with the path to the local work file 310 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath()); 311 312 } catch (Exception e) { 313 throw new GenericFileOperationFailedException("Cannot create new local work file: " + local); 314 } 315 316 boolean result; 317 try { 318 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 319 // store the java.io.File handle as the body 320 target.setBody(local); 321 result = client.retrieveFile(name, os); 322 323 } catch (IOException e) { 324 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 325 } finally { 326 // need to close the stream before rename it 327 IOHelper.close(os, "retrieve: " + name, log); 328 } 329 330 if (log.isDebugEnabled()) { 331 log.debug("Retrieve file to local work file result: " + result); 332 } 333 334 if (result) { 335 if (log.isTraceEnabled()) { 336 log.trace("Renaming local in progress file from: " + temp + " to: " + local); 337 } 338 // operation went okay so rename temp to local after we have retrieved the data 339 if (!FileUtil.renameFile(temp, local)) { 340 throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local); 341 } 342 } 343 344 return result; 345 } 346 347 public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 348 349 // if an existing file already exists what should we do? 350 if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) { 351 boolean existFile = existsFile(name); 352 if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) { 353 // ignore but indicate that the file was written 354 if (log.isTraceEnabled()) { 355 log.trace("An existing file already exists: " + name + ". Ignore and do not override it."); 356 } 357 return true; 358 } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) { 359 throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file."); 360 } 361 } 362 363 InputStream is = null; 364 try { 365 is = exchange.getIn().getMandatoryBody(InputStream.class); 366 if (endpoint.getFileExist() == GenericFileExist.Append) { 367 return client.appendFile(name, is); 368 } else { 369 return client.storeFile(name, is); 370 } 371 } catch (IOException e) { 372 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 373 } catch (InvalidPayloadException e) { 374 throw new GenericFileOperationFailedException("Cannot store file: " + name, e); 375 } finally { 376 IOHelper.close(is, "store: " + name, log); 377 } 378 } 379 380 public boolean existsFile(String name) throws GenericFileOperationFailedException { 381 // check whether a file already exists 382 String directory = FileUtil.onlyPath(name); 383 if (directory == null) { 384 return false; 385 } 386 387 String onlyName = FileUtil.stripPath(name); 388 try { 389 String[] names = client.listNames(directory); 390 // can return either null or an empty list depending on FTP servers 391 if (names == null) { 392 return false; 393 } 394 for (String existing : names) { 395 if (existing.equals(onlyName)) { 396 return true; 397 } 398 } 399 return false; 400 } catch (IOException e) { 401 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 402 } 403 } 404 405 public String getCurrentDirectory() throws GenericFileOperationFailedException { 406 try { 407 return client.printWorkingDirectory(); 408 } catch (IOException e) { 409 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 410 } 411 } 412 413 public void changeCurrentDirectory(String newDirectory) throws GenericFileOperationFailedException { 414 try { 415 client.changeWorkingDirectory(newDirectory); 416 } catch (IOException e) { 417 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 418 } 419 } 420 421 public List<FTPFile> listFiles() throws GenericFileOperationFailedException { 422 return listFiles("."); 423 } 424 425 public List<FTPFile> listFiles(String path) throws GenericFileOperationFailedException { 426 // use current directory if path not given 427 if (ObjectHelper.isEmpty(path)) { 428 path = "."; 429 } 430 431 try { 432 final List<FTPFile> list = new ArrayList<FTPFile>(); 433 FTPFile[] files = client.listFiles(path); 434 // can return either null or an empty list depending on FTP servers 435 if (files != null) { 436 list.addAll(Arrays.asList(files)); 437 } 438 return list; 439 } catch (IOException e) { 440 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 441 } 442 } 443 444 public boolean sendNoop() throws GenericFileOperationFailedException { 445 try { 446 return client.sendNoOp(); 447 } catch (IOException e) { 448 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 449 } 450 } 451 452 protected FTPClient getFtpClient() { 453 return client; 454 } 455 456 private boolean buildDirectoryChunks(String dirName) throws IOException { 457 final StringBuilder sb = new StringBuilder(dirName.length()); 458 final String[] dirs = dirName.split("/|\\\\"); 459 460 boolean success = false; 461 for (String dir : dirs) { 462 sb.append(dir).append('/'); 463 String directory = sb.toString(); 464 465 // do not try to build root / folder 466 if (!directory.equals("/")) { 467 if (log.isTraceEnabled()) { 468 log.trace("Trying to build remote directory by chunk: " + directory); 469 } 470 471 success = client.makeDirectory(directory); 472 } 473 } 474 475 return success; 476 } 477 478 }