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