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