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.Iterator; 028 import java.util.List; 029 030 import org.apache.camel.Exchange; 031 import org.apache.camel.InvalidPayloadException; 032 import org.apache.camel.component.file.FileComponent; 033 import org.apache.camel.component.file.GenericFile; 034 import org.apache.camel.component.file.GenericFileEndpoint; 035 import org.apache.camel.component.file.GenericFileExist; 036 import org.apache.camel.component.file.GenericFileOperationFailedException; 037 import org.apache.camel.util.FileUtil; 038 import org.apache.camel.util.IOHelper; 039 import org.apache.camel.util.ObjectHelper; 040 import org.apache.commons.logging.Log; 041 import org.apache.commons.logging.LogFactory; 042 import org.apache.commons.net.ftp.FTPClient; 043 import org.apache.commons.net.ftp.FTPClientConfig; 044 import org.apache.commons.net.ftp.FTPFile; 045 import org.apache.commons.net.ftp.FTPReply; 046 047 /** 048 * FTP remote file operations 049 */ 050 public class FtpOperations implements RemoteFileOperations<FTPFile> { 051 052 protected final transient Log log = LogFactory.getLog(getClass()); 053 protected final FTPClient client; 054 protected final FTPClientConfig clientConfig; 055 protected RemoteFileEndpoint<FTPFile> endpoint; 056 057 public FtpOperations(FTPClient client, FTPClientConfig clientConfig) { 058 this.client = client; 059 this.clientConfig = clientConfig; 060 } 061 062 public void setEndpoint(GenericFileEndpoint<FTPFile> endpoint) { 063 this.endpoint = (RemoteFileEndpoint<FTPFile>) endpoint; 064 } 065 066 public boolean connect(RemoteFileConfiguration configuration) throws GenericFileOperationFailedException { 067 if (log.isTraceEnabled()) { 068 log.trace("Connecting using FTPClient: " + client); 069 } 070 071 String host = configuration.getHost(); 072 int port = configuration.getPort(); 073 String username = configuration.getUsername(); 074 075 if (clientConfig != null) { 076 log.trace("Configuring FTPClient with config: " + clientConfig); 077 client.configure(clientConfig); 078 } 079 080 if (log.isTraceEnabled()) { 081 log.trace("Connecting to " + configuration.remoteServerInformation() + " using connection timeout: " + client.getConnectTimeout()); 082 } 083 084 boolean connected = false; 085 int attempt = 0; 086 087 while (!connected) { 088 try { 089 if (log.isTraceEnabled() && attempt > 0) { 090 log.trace("Reconnect attempt #" + attempt + " connecting to + " + configuration.remoteServerInformation()); 091 } 092 client.connect(host, port); 093 // must check reply code if we are connected 094 int reply = client.getReplyCode(); 095 096 if (FTPReply.isPositiveCompletion(reply)) { 097 // yes we could connect 098 connected = true; 099 } else { 100 // throw an exception to force the retry logic in the catch exception block 101 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), "Server refused connection"); 102 } 103 } catch (Exception e) { 104 // check if we are interrupted so we can break out 105 if (Thread.currentThread().isInterrupted()) { 106 throw new GenericFileOperationFailedException("Interrupted during connecting", new InterruptedException("Interrupted during connecting")); 107 } 108 109 GenericFileOperationFailedException failed; 110 if (e instanceof GenericFileOperationFailedException) { 111 failed = (GenericFileOperationFailedException) e; 112 } else { 113 failed = new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 114 } 115 116 if (log.isTraceEnabled()) { 117 log.trace("Cannot connect due: " + failed.getMessage()); 118 } 119 attempt++; 120 if (attempt > endpoint.getMaximumReconnectAttempts()) { 121 throw failed; 122 } 123 if (endpoint.getReconnectDelay() > 0) { 124 try { 125 Thread.sleep(endpoint.getReconnectDelay()); 126 } catch (InterruptedException ie) { 127 // we could potentially also be interrupted during sleep 128 Thread.currentThread().interrupt(); 129 throw new GenericFileOperationFailedException("Interrupted during sleeping", ie); 130 } 131 } 132 } 133 } 134 135 // must enter passive mode directly after connect 136 if (configuration.isPassiveMode()) { 137 log.trace("Using passive mode connections"); 138 client.enterLocalPassiveMode(); 139 } 140 141 // must set soTimeout after connect 142 if (endpoint instanceof FtpEndpoint) { 143 FtpEndpoint ftpEndpoint = (FtpEndpoint) endpoint; 144 if (ftpEndpoint.getSoTimeout() > 0) { 145 log.trace("Using SoTimeout=" + ftpEndpoint.getSoTimeout()); 146 try { 147 client.setSoTimeout(ftpEndpoint.getSoTimeout()); 148 } catch (IOException e) { 149 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 150 } 151 } 152 } 153 154 try { 155 boolean login; 156 if (username != null) { 157 if (log.isTraceEnabled()) { 158 log.trace("Attempting to login user: " + username + " using password: " + configuration.getPassword()); 159 } 160 login = client.login(username, configuration.getPassword()); 161 } else { 162 log.trace("Attempting to login anonymous"); 163 login = client.login("anonymous", ""); 164 } 165 if (log.isTraceEnabled()) { 166 log.trace("User " + (username != null ? username : "anonymous") + " logged in: " + login); 167 } 168 if (!login) { 169 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString()); 170 } 171 client.setFileType(configuration.isBinary() ? FTPClient.BINARY_FILE_TYPE : FTPClient.ASCII_FILE_TYPE); 172 } catch (IOException e) { 173 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 174 } 175 176 // site commands 177 if (endpoint.getConfiguration().getSiteCommand() != null) { 178 // commands can be separated using new line 179 Iterator it = ObjectHelper.createIterator(endpoint.getConfiguration().getSiteCommand(), "\n"); 180 while (it.hasNext()) { 181 Object next = it.next(); 182 String command = endpoint.getCamelContext().getTypeConverter().convertTo(String.class, next); 183 if (log.isTraceEnabled()) { 184 log.trace("Site command to send: " + command); 185 } 186 if (command != null) { 187 boolean result = sendSiteCommand(command); 188 if (!result) { 189 throw new GenericFileOperationFailedException("Site command: " + command + " returned false"); 190 } 191 } 192 } 193 } 194 195 return true; 196 } 197 198 public boolean isConnected() throws GenericFileOperationFailedException { 199 return client.isConnected(); 200 } 201 202 public void disconnect() throws GenericFileOperationFailedException { 203 // logout before disconnecting 204 try { 205 client.logout(); 206 } catch (IOException e) { 207 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 208 } finally { 209 try { 210 client.disconnect(); 211 } catch (IOException e) { 212 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 213 } 214 } 215 } 216 217 public boolean deleteFile(String name) throws GenericFileOperationFailedException { 218 if (log.isDebugEnabled()) { 219 log.debug("Deleting file: " + name); 220 } 221 try { 222 return this.client.deleteFile(name); 223 } catch (IOException e) { 224 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 225 } 226 } 227 228 public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { 229 if (log.isDebugEnabled()) { 230 log.debug("Renaming file: " + from + " to: " + to); 231 } 232 try { 233 return client.rename(from, to); 234 } catch (IOException e) { 235 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 236 } 237 } 238 239 public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { 240 if (log.isTraceEnabled()) { 241 log.trace("buildDirectory(" + directory + ")"); 242 } 243 try { 244 String originalDirectory = client.printWorkingDirectory(); 245 246 boolean success; 247 try { 248 // maybe the full directory already exists 249 success = client.changeWorkingDirectory(directory); 250 if (!success) { 251 if (log.isTraceEnabled()) { 252 log.trace("Trying to build remote directory: " + directory); 253 } 254 success = client.makeDirectory(directory); 255 if (!success) { 256 // we are here if the server side doesn't create intermediate folders so create the folder one by one 257 success = buildDirectoryChunks(directory); 258 } 259 } 260 261 return success; 262 } finally { 263 // change back to original directory 264 if (originalDirectory != null) { 265 client.changeWorkingDirectory(originalDirectory); 266 } 267 } 268 } catch (IOException e) { 269 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 270 } 271 } 272 273 public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 274 if (log.isTraceEnabled()) { 275 log.trace("retrieveFile(" + name + ")"); 276 } 277 if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) { 278 // local work directory is configured so we should store file content as files in this local directory 279 return retrieveFileToFileInLocalWorkDirectory(name, exchange); 280 } else { 281 // store file content directory as stream on the body 282 return retrieveFileToStreamInBody(name, exchange); 283 } 284 } 285 286 @SuppressWarnings("unchecked") 287 private boolean retrieveFileToStreamInBody(String name, Exchange exchange) throws GenericFileOperationFailedException { 288 OutputStream os = null; 289 boolean result; 290 try { 291 os = new ByteArrayOutputStream(); 292 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 293 ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set"); 294 target.setBody(os); 295 296 // remember current directory 297 String currentDir = getCurrentDirectory(); 298 299 // change directory to path where the file is to be retrieved 300 // (must do this as some FTP servers cannot retrieve using absolute path) 301 String path = FileUtil.onlyPath(name); 302 if (path != null) { 303 changeCurrentDirectory(path); 304 } 305 String onlyName = FileUtil.stripPath(name); 306 307 result = client.retrieveFile(onlyName, os); 308 309 // change back to current directory 310 changeCurrentDirectory(currentDir); 311 312 } catch (IOException e) { 313 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 314 } finally { 315 IOHelper.close(os, "retrieve: " + name, log); 316 } 317 318 return result; 319 } 320 321 @SuppressWarnings("unchecked") 322 private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException { 323 File temp; 324 File local = new File(FileUtil.normalizePath(endpoint.getLocalWorkDirectory())); 325 OutputStream os; 326 try { 327 // use relative filename in local work directory 328 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 329 ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set"); 330 String relativeName = target.getRelativeFilePath(); 331 332 temp = new File(local, relativeName + ".inprogress"); 333 local = new File(local, relativeName); 334 335 // create directory to local work file 336 local.mkdirs(); 337 338 // delete any existing files 339 if (temp.exists()) { 340 if (!FileUtil.deleteFile(temp)) { 341 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp); 342 } 343 } 344 if (local.exists()) { 345 if (!FileUtil.deleteFile(local)) { 346 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local); 347 } 348 } 349 350 // create new temp local work file 351 if (!temp.createNewFile()) { 352 throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp); 353 } 354 355 // store content as a file in the local work directory in the temp handle 356 os = new FileOutputStream(temp); 357 358 // set header with the path to the local work file 359 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath()); 360 361 } catch (Exception e) { 362 throw new GenericFileOperationFailedException("Cannot create new local work file: " + local); 363 } 364 365 boolean result; 366 try { 367 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 368 // store the java.io.File handle as the body 369 target.setBody(local); 370 371 // remember current directory 372 String currentDir = getCurrentDirectory(); 373 374 // change directory to path where the file is to be retrieved 375 // (must do this as some FTP servers cannot retrieve using absolute path) 376 String path = FileUtil.onlyPath(name); 377 if (path != null) { 378 changeCurrentDirectory(path); 379 } 380 String onlyName = FileUtil.stripPath(name); 381 382 result = client.retrieveFile(onlyName, os); 383 384 // change back to current directory 385 changeCurrentDirectory(currentDir); 386 387 } catch (IOException e) { 388 if (log.isTraceEnabled()) { 389 log.trace("Error occurred during retrieving file: " + name + " to local directory. Deleting local work file: " + temp); 390 } 391 // failed to retrieve the file so we need to close streams and delete in progress file 392 // must close stream before deleting file 393 IOHelper.close(os, "retrieve: " + name, log); 394 boolean deleted = FileUtil.deleteFile(temp); 395 if (!deleted) { 396 log.warn("Error occurred during retrieving file: " + name + " to local directory. Cannot delete local work file: " + temp); 397 } 398 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 399 } finally { 400 // need to close the stream before rename it 401 IOHelper.close(os, "retrieve: " + name, log); 402 } 403 404 if (log.isDebugEnabled()) { 405 log.debug("Retrieve file to local work file result: " + result); 406 } 407 408 if (result) { 409 if (log.isTraceEnabled()) { 410 log.trace("Renaming local in progress file from: " + temp + " to: " + local); 411 } 412 // operation went okay so rename temp to local after we have retrieved the data 413 if (!FileUtil.renameFile(temp, local)) { 414 throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local); 415 } 416 } 417 418 return result; 419 } 420 421 public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 422 if (log.isTraceEnabled()) { 423 log.trace("storeFile(" + name + ")"); 424 } 425 426 // if an existing file already exists what should we do? 427 if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) { 428 boolean existFile = existsFile(name); 429 if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) { 430 // ignore but indicate that the file was written 431 if (log.isTraceEnabled()) { 432 log.trace("An existing file already exists: " + name + ". Ignore and do not override it."); 433 } 434 return true; 435 } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) { 436 throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file."); 437 } 438 } 439 440 InputStream is = null; 441 try { 442 is = exchange.getIn().getMandatoryBody(InputStream.class); 443 if (endpoint.getFileExist() == GenericFileExist.Append) { 444 return client.appendFile(name, is); 445 } else { 446 return client.storeFile(name, is); 447 } 448 } catch (IOException e) { 449 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 450 } catch (InvalidPayloadException e) { 451 throw new GenericFileOperationFailedException("Cannot store file: " + name, e); 452 } finally { 453 IOHelper.close(is, "store: " + name, log); 454 } 455 } 456 457 public boolean existsFile(String name) throws GenericFileOperationFailedException { 458 if (log.isTraceEnabled()) { 459 log.trace("existsFile(" + name + ")"); 460 } 461 462 // check whether a file already exists 463 String directory = FileUtil.onlyPath(name); 464 String onlyName = FileUtil.stripPath(name); 465 try { 466 String[] names; 467 if (directory != null) { 468 names = client.listNames(directory); 469 } else { 470 names = client.listNames(); 471 } 472 // can return either null or an empty list depending on FTP servers 473 if (names == null) { 474 return false; 475 } 476 for (String existing : names) { 477 if (existing.equals(onlyName)) { 478 return true; 479 } 480 } 481 return false; 482 } catch (IOException e) { 483 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 484 } 485 } 486 487 public String getCurrentDirectory() throws GenericFileOperationFailedException { 488 if (log.isTraceEnabled()) { 489 log.trace("getCurrentDirectory()"); 490 } 491 try { 492 return client.printWorkingDirectory(); 493 } catch (IOException e) { 494 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 495 } 496 } 497 498 public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { 499 if (ObjectHelper.isEmpty(path)) { 500 return; 501 } 502 503 // if it starts with the root path then a little special handling for that 504 if (FileUtil.hasLeadingSeparator(path)) { 505 // change to root path 506 doChangeDirectory(path.substring(0, 1)); 507 path = path.substring(1); 508 } 509 510 // split into multiple dirs 511 final String[] dirs = path.split("/|\\\\"); 512 513 if (dirs == null || dirs.length == 0) { 514 // path was just a relative single path 515 doChangeDirectory(path); 516 return; 517 } 518 519 // there are multiple dirs so do this in chunks 520 for (String dir : dirs) { 521 doChangeDirectory(dir); 522 } 523 } 524 525 private void doChangeDirectory(String path) { 526 if (path == null || ".".equals(path) || ObjectHelper.isEmpty(path)) { 527 return; 528 } 529 530 if (log.isTraceEnabled()) { 531 log.trace("Changing directory: " + path); 532 } 533 boolean success; 534 try { 535 success = client.changeWorkingDirectory(path); 536 } catch (IOException e) { 537 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 538 } 539 if (!success) { 540 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), "Cannot change directory to: " + path); 541 } 542 } 543 544 public void changeToParentDirectory() throws GenericFileOperationFailedException { 545 try { 546 client.changeToParentDirectory(); 547 } catch (IOException e) { 548 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 549 } 550 } 551 552 public List<FTPFile> listFiles() throws GenericFileOperationFailedException { 553 if (log.isTraceEnabled()) { 554 log.trace("listFiles()"); 555 } 556 try { 557 final List<FTPFile> list = new ArrayList<FTPFile>(); 558 FTPFile[] files = client.listFiles(); 559 // can return either null or an empty list depending on FTP servers 560 if (files != null) { 561 list.addAll(Arrays.asList(files)); 562 } 563 return list; 564 } catch (IOException e) { 565 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 566 } 567 } 568 569 public List<FTPFile> listFiles(String path) throws GenericFileOperationFailedException { 570 if (log.isTraceEnabled()) { 571 log.trace("listFiles(" + path + ")"); 572 } 573 // use current directory if path not given 574 if (ObjectHelper.isEmpty(path)) { 575 path = "."; 576 } 577 578 try { 579 final List<FTPFile> list = new ArrayList<FTPFile>(); 580 FTPFile[] files = client.listFiles(path); 581 // can return either null or an empty list depending on FTP servers 582 if (files != null) { 583 list.addAll(Arrays.asList(files)); 584 } 585 return list; 586 } catch (IOException e) { 587 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 588 } 589 } 590 591 public boolean sendNoop() throws GenericFileOperationFailedException { 592 if (log.isTraceEnabled()) { 593 log.trace("sendNoOp"); 594 } 595 try { 596 return client.sendNoOp(); 597 } catch (IOException e) { 598 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 599 } 600 } 601 602 public boolean sendSiteCommand(String command) throws GenericFileOperationFailedException { 603 if (log.isTraceEnabled()) { 604 log.trace("sendSiteCommand(" + command + ")"); 605 } 606 try { 607 return client.sendSiteCommand(command); 608 } catch (IOException e) { 609 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 610 } 611 } 612 613 protected FTPClient getFtpClient() { 614 return client; 615 } 616 617 private boolean buildDirectoryChunks(String dirName) throws IOException { 618 final StringBuilder sb = new StringBuilder(dirName.length()); 619 final String[] dirs = dirName.split("/|\\\\"); 620 621 boolean success = false; 622 for (String dir : dirs) { 623 sb.append(dir).append('/'); 624 String directory = sb.toString(); 625 626 // do not try to build root / folder 627 if (!directory.equals("/")) { 628 if (log.isTraceEnabled()) { 629 log.trace("Trying to build remote directory by chunk: " + directory); 630 } 631 632 success = client.makeDirectory(directory); 633 } 634 } 635 636 return success; 637 } 638 639 }