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.component.file.FileComponent; 031 import org.apache.camel.component.file.GenericFile; 032 import org.apache.camel.component.file.GenericFileEndpoint; 033 import org.apache.camel.component.file.GenericFileExist; 034 import org.apache.camel.component.file.GenericFileOperationFailedException; 035 import org.apache.camel.util.FileUtil; 036 import org.apache.camel.util.ObjectHelper; 037 import org.apache.commons.logging.Log; 038 import org.apache.commons.logging.LogFactory; 039 import org.apache.commons.net.ftp.FTPClient; 040 import org.apache.commons.net.ftp.FTPFile; 041 import org.apache.commons.net.ftp.FTPReply; 042 043 /** 044 * FTP remote file operations 045 */ 046 public class FtpOperations implements RemoteFileOperations<FTPFile> { 047 private static final transient Log LOG = LogFactory.getLog(FtpOperations.class); 048 private final FTPClient client; 049 private RemoteFileEndpoint endpoint; 050 051 public FtpOperations() { 052 this.client = new FTPClient(); 053 } 054 055 public FtpOperations(FTPClient client) { 056 this.client = client; 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 FtpConfiguration ftpConfig = (FtpConfiguration) configuration; 073 074 if (ftpConfig.getFtpClientConfig() != null) { 075 LOG.trace("Configuring FTPClient with config: " + ftpConfig.getFtpClientConfig()); 076 client.configure(ftpConfig.getFtpClientConfig()); 077 } 078 079 if (LOG.isTraceEnabled()) { 080 LOG.trace("Connecting to " + configuration.remoteServerInformation()); 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("Could not 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 try { 134 boolean login; 135 if (username != null) { 136 if (LOG.isTraceEnabled()) { 137 LOG.trace("Attempting to login user: " + username + " using password: " + configuration.getPassword()); 138 } 139 login = client.login(username, configuration.getPassword()); 140 } else { 141 LOG.trace("Attempting to login anonymous"); 142 login = client.login("anonymous", null); 143 } 144 if (LOG.isTraceEnabled()) { 145 LOG.trace("User " + (username != null ? username : "anonymous") + " logged in: " + login); 146 } 147 if (!login) { 148 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString()); 149 } 150 client.setFileType(configuration.isBinary() ? FTPClient.BINARY_FILE_TYPE : FTPClient.ASCII_FILE_TYPE); 151 } catch (IOException e) { 152 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 153 } 154 155 return true; 156 } 157 158 public boolean isConnected() throws GenericFileOperationFailedException { 159 return client.isConnected(); 160 } 161 162 public void disconnect() throws GenericFileOperationFailedException { 163 // logout before disconnecting 164 try { 165 client.logout(); 166 } catch (IOException e) { 167 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 168 } finally { 169 try { 170 client.disconnect(); 171 } catch (IOException e) { 172 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 173 } 174 } 175 } 176 177 public boolean deleteFile(String name) throws GenericFileOperationFailedException { 178 if (LOG.isDebugEnabled()) { 179 LOG.debug("Deleteing file: " + name); 180 } 181 try { 182 return this.client.deleteFile(name); 183 } catch (IOException e) { 184 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 185 } 186 } 187 188 public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { 189 if (LOG.isDebugEnabled()) { 190 LOG.debug("Renaming file: " + from + " to: " + to); 191 } 192 try { 193 return client.rename(from, to); 194 } catch (IOException e) { 195 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 196 } 197 } 198 199 public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { 200 if (LOG.isTraceEnabled()) { 201 LOG.trace("Building directory: " + directory); 202 } 203 try { 204 String originalDirectory = client.printWorkingDirectory(); 205 206 boolean success; 207 try { 208 // maybe the full directory already exsits 209 success = client.changeWorkingDirectory(directory); 210 if (!success) { 211 if (LOG.isTraceEnabled()) { 212 LOG.trace("Trying to build remote directory: " + directory); 213 } 214 success = client.makeDirectory(directory); 215 if (!success) { 216 // we are here if the server side doesn't create intermediate folders so create the folder one by one 217 success = buildDirectoryChunks(directory); 218 } 219 } 220 221 return success; 222 } finally { 223 // change back to original directory 224 if (originalDirectory != null) { 225 client.changeWorkingDirectory(originalDirectory); 226 } 227 } 228 } catch (IOException e) { 229 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 230 } 231 } 232 233 public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 234 if (ObjectHelper.isNotEmpty(endpoint.getLocalWorkDirectory())) { 235 // local work directory is configured so we should store file content as files in this local directory 236 return retrieveFileToFileInLocalWorkDirectory(name, exchange); 237 } else { 238 // store file content directory as stream on the body 239 return retrieveFileToStreamInBody(name, exchange); 240 } 241 } 242 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 private boolean retrieveFileToFileInLocalWorkDirectory(String name, Exchange exchange) throws GenericFileOperationFailedException { 259 File temp; 260 File local = new File(FileUtil.normalizePath(endpoint.getLocalWorkDirectory())); 261 OutputStream os; 262 try { 263 // use relative filename in local work directory 264 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 265 ObjectHelper.notNull(target, "Exchange should have the " + FileComponent.FILE_EXCHANGE_FILE + " set"); 266 String relativeName = target.getRelativeFilePath(); 267 268 temp = new File(local, relativeName + ".inprogress"); 269 local = new File(local, relativeName); 270 271 // create directory to local work file 272 local.mkdirs(); 273 274 // delete any existing files 275 if (temp.exists()) { 276 if (!temp.delete()) { 277 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + temp); 278 } 279 } 280 if (local.exists()) { 281 if (!local.delete()) { 282 throw new GenericFileOperationFailedException("Cannot delete existing local work file: " + local); 283 } 284 } 285 286 // create new temp local work file 287 if (!temp.createNewFile()) { 288 throw new GenericFileOperationFailedException("Cannot create new local work file: " + temp); 289 } 290 291 // store content as a file in the local work directory in the temp handle 292 os = new FileOutputStream(temp); 293 294 // set header with the path to the local work file 295 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, local.getPath()); 296 297 } catch (Exception e) { 298 throw new GenericFileOperationFailedException("Cannot create new local work file: " + local); 299 } 300 301 boolean result; 302 try { 303 GenericFile<FTPFile> target = (GenericFile<FTPFile>) exchange.getProperty(FileComponent.FILE_EXCHANGE_FILE); 304 // store the java.io.File handle as the body 305 target.setBody(local); 306 result = client.retrieveFile(name, os); 307 308 } catch (IOException e) { 309 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 310 } finally { 311 // need to close the stream before rename it 312 ObjectHelper.close(os, "retrieve: " + name, LOG); 313 } 314 315 // rename temp to local after we have retrieved the data 316 if (!temp.renameTo(local)) { 317 throw new GenericFileOperationFailedException("Cannot rename local work file from: " + temp + " to: " + local); 318 } 319 320 321 return result; 322 } 323 324 public boolean storeFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 325 326 // if an existing file already exists what should we do? 327 if (endpoint.getFileExist() == GenericFileExist.Ignore || endpoint.getFileExist() == GenericFileExist.Fail) { 328 boolean existFile = existsFile(name); 329 if (existFile && endpoint.getFileExist() == GenericFileExist.Ignore) { 330 // ignore but indicate that the file was written 331 if (LOG.isTraceEnabled()) { 332 LOG.trace("An existing file already exists: " + name + ". Ignore and do not override it."); 333 } 334 return true; 335 } else if (existFile && endpoint.getFileExist() == GenericFileExist.Fail) { 336 throw new GenericFileOperationFailedException("File already exist: " + name + ". Cannot write new file."); 337 } 338 } 339 340 InputStream is = exchange.getIn().getBody(InputStream.class); 341 try { 342 if (endpoint.getFileExist() == GenericFileExist.Append) { 343 return client.appendFile(name, is); 344 } else { 345 return client.storeFile(name, is); 346 } 347 } catch (IOException e) { 348 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 349 } finally { 350 ObjectHelper.close(is, "store: " + name, LOG); 351 } 352 } 353 354 public boolean existsFile(String name) throws GenericFileOperationFailedException { 355 // check whether a file already exists 356 String directory = FileUtil.onlyPath(name); 357 if (directory == null) { 358 return false; 359 } 360 361 String onlyName = FileUtil.stripPath(name); 362 try { 363 String[] names = client.listNames(directory); 364 for (String existing : names) { 365 if (existing.equals(onlyName)) { 366 return true; 367 } 368 } 369 return false; 370 } catch (IOException e) { 371 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 372 } 373 } 374 375 public String getCurrentDirectory() throws GenericFileOperationFailedException { 376 try { 377 return client.printWorkingDirectory(); 378 } catch (IOException e) { 379 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 380 } 381 } 382 383 public void changeCurrentDirectory(String newDirectory) throws GenericFileOperationFailedException { 384 try { 385 client.changeWorkingDirectory(newDirectory); 386 } catch (IOException e) { 387 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 388 } 389 } 390 391 public List<FTPFile> listFiles() throws GenericFileOperationFailedException { 392 return listFiles("."); 393 } 394 395 public List<FTPFile> listFiles(String path) throws GenericFileOperationFailedException { 396 // use current directory if path not given 397 if (ObjectHelper.isEmpty(path)) { 398 path = "."; 399 } 400 401 try { 402 final List<FTPFile> list = new ArrayList<FTPFile>(); 403 FTPFile[] files = client.listFiles(path); 404 list.addAll(Arrays.asList(files)); 405 return list; 406 } catch (IOException e) { 407 throw new GenericFileOperationFailedException(client.getReplyCode(), client.getReplyString(), e.getMessage(), e); 408 } 409 } 410 411 private boolean buildDirectoryChunks(String dirName) throws IOException { 412 final StringBuilder sb = new StringBuilder(dirName.length()); 413 final String[] dirs = dirName.split("/|\\\\"); 414 415 boolean success = false; 416 for (String dir : dirs) { 417 sb.append(dir).append('/'); 418 String directory = sb.toString(); 419 420 // do not try to build root / folder 421 if (!directory.equals("/")) { 422 if (LOG.isTraceEnabled()) { 423 LOG.trace("Trying to build remote directory by chunk: " + directory); 424 } 425 426 success = client.makeDirectory(directory); 427 } 428 } 429 430 return success; 431 } 432 433 }