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 */ 017package org.apache.camel.component.file; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.FileOutputStream; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.InputStreamReader; 025import java.io.RandomAccessFile; 026import java.io.Reader; 027import java.io.Writer; 028import java.nio.ByteBuffer; 029import java.nio.channels.FileChannel; 030import java.nio.file.Files; 031import java.nio.file.attribute.PosixFilePermission; 032import java.nio.file.attribute.PosixFilePermissions; 033import java.util.Date; 034import java.util.List; 035import java.util.Set; 036 037import org.apache.camel.Exchange; 038import org.apache.camel.InvalidPayloadException; 039import org.apache.camel.WrappedFile; 040import org.apache.camel.converter.IOConverter; 041import org.apache.camel.util.FileUtil; 042import org.apache.camel.util.IOHelper; 043import org.apache.camel.util.ObjectHelper; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047/** 048 * File operations for {@link java.io.File}. 049 */ 050public class FileOperations implements GenericFileOperations<File> { 051 private static final Logger LOG = LoggerFactory.getLogger(FileOperations.class); 052 private FileEndpoint endpoint; 053 054 public FileOperations() { 055 } 056 057 public FileOperations(FileEndpoint endpoint) { 058 this.endpoint = endpoint; 059 } 060 061 public void setEndpoint(GenericFileEndpoint<File> endpoint) { 062 this.endpoint = (FileEndpoint) endpoint; 063 } 064 065 public boolean deleteFile(String name) throws GenericFileOperationFailedException { 066 File file = new File(name); 067 return FileUtil.deleteFile(file); 068 } 069 070 public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { 071 boolean renamed = false; 072 File file = new File(from); 073 File target = new File(to); 074 try { 075 if (endpoint.isRenameUsingCopy()) { 076 renamed = FileUtil.renameFileUsingCopy(file, target); 077 } else { 078 renamed = FileUtil.renameFile(file, target, endpoint.isCopyAndDeleteOnRenameFail()); 079 } 080 } catch (IOException e) { 081 throw new GenericFileOperationFailedException("Error renaming file from " + from + " to " + to, e); 082 } 083 084 return renamed; 085 } 086 087 public boolean existsFile(String name) throws GenericFileOperationFailedException { 088 File file = new File(name); 089 return file.exists(); 090 } 091 092 public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { 093 ObjectHelper.notNull(endpoint, "endpoint"); 094 095 // always create endpoint defined directory 096 if (endpoint.isAutoCreate() && !endpoint.getFile().exists()) { 097 LOG.trace("Building starting directory: {}", endpoint.getFile()); 098 endpoint.getFile().mkdirs(); 099 } 100 101 if (ObjectHelper.isEmpty(directory)) { 102 // no directory to build so return true to indicate ok 103 return true; 104 } 105 106 File endpointPath = endpoint.getFile(); 107 File target = new File(directory); 108 109 File path; 110 if (absolute) { 111 // absolute path 112 path = target; 113 } else if (endpointPath.equals(target)) { 114 // its just the root of the endpoint path 115 path = endpointPath; 116 } else { 117 // relative after the endpoint path 118 String afterRoot = ObjectHelper.after(directory, endpointPath.getPath() + File.separator); 119 if (ObjectHelper.isNotEmpty(afterRoot)) { 120 // dir is under the root path 121 path = new File(endpoint.getFile(), afterRoot); 122 } else { 123 // dir is relative to the root path 124 path = new File(endpoint.getFile(), directory); 125 } 126 } 127 128 // We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time. 129 synchronized (this) { 130 if (path.isDirectory() && path.exists()) { 131 // the directory already exists 132 return true; 133 } else { 134 if (LOG.isTraceEnabled()) { 135 LOG.trace("Building directory: {}", path); 136 } 137 return path.mkdirs(); 138 } 139 } 140 } 141 142 public List<File> listFiles() throws GenericFileOperationFailedException { 143 // noop 144 return null; 145 } 146 147 public List<File> listFiles(String path) throws GenericFileOperationFailedException { 148 // noop 149 return null; 150 } 151 152 public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { 153 // noop 154 } 155 156 public void changeToParentDirectory() throws GenericFileOperationFailedException { 157 // noop 158 } 159 160 public String getCurrentDirectory() throws GenericFileOperationFailedException { 161 // noop 162 return null; 163 } 164 165 public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { 166 // noop as we use type converters to read the body content for java.io.File 167 return true; 168 } 169 170 @Override 171 public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException { 172 // noop as we used type converters to read the body content for java.io.File 173 } 174 175 public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException { 176 ObjectHelper.notNull(endpoint, "endpoint"); 177 178 File file = new File(fileName); 179 180 // if an existing file already exists what should we do? 181 if (file.exists()) { 182 if (endpoint.getFileExist() == GenericFileExist.Ignore) { 183 // ignore but indicate that the file was written 184 LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file); 185 return true; 186 } else if (endpoint.getFileExist() == GenericFileExist.Fail) { 187 throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file."); 188 } else if (endpoint.getFileExist() == GenericFileExist.Move) { 189 // move any existing file first 190 doMoveExistingFile(fileName); 191 } 192 } 193 194 // Do an explicit test for a null body and decide what to do 195 if (exchange.getIn().getBody() == null) { 196 if (endpoint.isAllowNullBody()) { 197 LOG.trace("Writing empty file."); 198 try { 199 writeFileEmptyBody(file); 200 return true; 201 } catch (IOException e) { 202 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 203 } 204 } else { 205 throw new GenericFileOperationFailedException("Cannot write null body to file: " + file); 206 } 207 } 208 209 // we can write the file by 3 different techniques 210 // 1. write file to file 211 // 2. rename a file from a local work path 212 // 3. write stream to file 213 try { 214 215 // is there an explicit charset configured we must write the file as 216 String charset = endpoint.getCharset(); 217 218 // we can optimize and use file based if no charset must be used, and the input body is a file 219 File source = null; 220 boolean fileBased = false; 221 if (charset == null) { 222 // if no charset, then we can try using file directly (optimized) 223 Object body = exchange.getIn().getBody(); 224 if (body instanceof WrappedFile) { 225 body = ((WrappedFile<?>) body).getFile(); 226 } 227 if (body instanceof File) { 228 source = (File) body; 229 fileBased = true; 230 } 231 } 232 233 if (fileBased) { 234 // okay we know the body is a file based 235 236 // so try to see if we can optimize by renaming the local work path file instead of doing 237 // a full file to file copy, as the local work copy is to be deleted afterwards anyway 238 // local work path 239 File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class); 240 if (local != null && local.exists()) { 241 boolean renamed = writeFileByLocalWorkPath(local, file); 242 if (renamed) { 243 // try to keep last modified timestamp if configured to do so 244 keepLastModified(exchange, file); 245 // set permissions if the chmod option was set 246 if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { 247 Set<PosixFilePermission> permissions = endpoint.getPermissions(); 248 if (!permissions.isEmpty()) { 249 Files.setPosixFilePermissions(file.toPath(), permissions); 250 LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file); 251 } 252 } 253 // clear header as we have renamed the file 254 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null); 255 // return as the operation is complete, we just renamed the local work file 256 // to the target. 257 return true; 258 } 259 } else if (source != null && source.exists()) { 260 // no there is no local work file so use file to file copy if the source exists 261 writeFileByFile(source, file); 262 // try to keep last modified timestamp if configured to do so 263 keepLastModified(exchange, file); 264 // set permissions if the chmod option was set 265 if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { 266 Set<PosixFilePermission> permissions = endpoint.getPermissions(); 267 if (!permissions.isEmpty()) { 268 Files.setPosixFilePermissions(file.toPath(), permissions); 269 LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file); 270 } 271 } 272 return true; 273 } 274 } 275 276 if (charset != null) { 277 // charset configured so we must use a reader so we can write with encoding 278 Reader in = exchange.getContext().getTypeConverter().tryConvertTo(Reader.class, exchange, exchange.getIn().getBody()); 279 if (in == null) { 280 // okay no direct reader conversion, so use an input stream (which a lot can be converted as) 281 InputStream is = exchange.getIn().getMandatoryBody(InputStream.class); 282 in = new InputStreamReader(is); 283 } 284 // buffer the reader 285 in = IOHelper.buffered(in); 286 writeFileByReaderWithCharset(in, file, charset); 287 } else { 288 // fallback and use stream based 289 InputStream in = exchange.getIn().getMandatoryBody(InputStream.class); 290 writeFileByStream(in, file); 291 } 292 293 // try to keep last modified timestamp if configured to do so 294 keepLastModified(exchange, file); 295 // set permissions if the chmod option was set 296 if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { 297 Set<PosixFilePermission> permissions = endpoint.getPermissions(); 298 if (!permissions.isEmpty()) { 299 Files.setPosixFilePermissions(file.toPath(), permissions); 300 LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file); 301 } 302 } 303 304 return true; 305 } catch (IOException e) { 306 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 307 } catch (InvalidPayloadException e) { 308 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 309 } 310 } 311 312 /** 313 * Moves any existing file due fileExists=Move is in use. 314 */ 315 private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException { 316 // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes 317 // create a dummy exchange as Exchange is needed for expression evaluation 318 // we support only the following 3 tokens. 319 Exchange dummy = endpoint.createExchange(); 320 String parent = FileUtil.onlyPath(fileName); 321 String onlyName = FileUtil.stripPath(fileName); 322 dummy.getIn().setHeader(Exchange.FILE_NAME, fileName); 323 dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName); 324 dummy.getIn().setHeader(Exchange.FILE_PARENT, parent); 325 326 String to = endpoint.getMoveExisting().evaluate(dummy, String.class); 327 // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File) 328 to = FileUtil.normalizePath(to); 329 if (ObjectHelper.isEmpty(to)) { 330 throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName); 331 } 332 333 // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting) 334 // use java.io.File to compute the file path 335 File toFile = new File(to); 336 String directory = toFile.getParent(); 337 boolean absolute = FileUtil.isAbsolute(toFile); 338 if (directory != null) { 339 if (!buildDirectory(directory, absolute)) { 340 LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory); 341 } 342 } 343 344 // deal if there already exists a file 345 if (existsFile(to)) { 346 if (endpoint.isEagerDeleteTargetFile()) { 347 LOG.trace("Deleting existing file: {}", to); 348 if (!deleteFile(to)) { 349 throw new GenericFileOperationFailedException("Cannot delete file: " + to); 350 } 351 } else { 352 throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to); 353 } 354 } 355 356 LOG.trace("Moving existing file: {} to: {}", fileName, to); 357 if (!renameFile(fileName, to)) { 358 throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to); 359 } 360 } 361 362 private void keepLastModified(Exchange exchange, File file) { 363 if (endpoint.isKeepLastModified()) { 364 Long last; 365 Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class); 366 if (date != null) { 367 last = date.getTime(); 368 } else { 369 // fallback and try a long 370 last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class); 371 } 372 if (last != null) { 373 boolean result = file.setLastModified(last); 374 if (LOG.isTraceEnabled()) { 375 LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result}); 376 } 377 } 378 } 379 } 380 381 private boolean writeFileByLocalWorkPath(File source, File file) throws IOException { 382 LOG.trace("Using local work file being renamed from: {} to: {}", source, file); 383 return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail()); 384 } 385 386 private void writeFileByFile(File source, File target) throws IOException { 387 FileChannel in = new FileInputStream(source).getChannel(); 388 FileChannel out = null; 389 try { 390 out = prepareOutputFileChannel(target); 391 LOG.debug("Using FileChannel to write file: {}", target); 392 long size = in.size(); 393 long position = 0; 394 while (position < size) { 395 position += in.transferTo(position, endpoint.getBufferSize(), out); 396 } 397 } finally { 398 IOHelper.close(in, source.getName(), LOG); 399 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 400 } 401 } 402 403 private void writeFileByStream(InputStream in, File target) throws IOException { 404 FileChannel out = null; 405 try { 406 out = prepareOutputFileChannel(target); 407 LOG.debug("Using InputStream to write file: {}", target); 408 int size = endpoint.getBufferSize(); 409 byte[] buffer = new byte[size]; 410 ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); 411 int bytesRead; 412 while ((bytesRead = in.read(buffer)) != -1) { 413 if (bytesRead < size) { 414 byteBuffer.limit(bytesRead); 415 } 416 out.write(byteBuffer); 417 byteBuffer.clear(); 418 } 419 } finally { 420 IOHelper.close(in, target.getName(), LOG); 421 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 422 } 423 } 424 425 private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException { 426 boolean append = endpoint.getFileExist() == GenericFileExist.Append; 427 FileOutputStream os = new FileOutputStream(target, append); 428 Writer out = IOConverter.toWriter(os, charset); 429 try { 430 LOG.debug("Using Reader to write file: {} with charset: {}", target, charset); 431 int size = endpoint.getBufferSize(); 432 IOHelper.copy(in, out, size); 433 } finally { 434 IOHelper.close(in, target.getName(), LOG); 435 IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites()); 436 } 437 } 438 439 /** 440 * Creates a new file if the file doesn't exist. 441 * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated 442 */ 443 private void writeFileEmptyBody(File target) throws IOException { 444 if (!target.exists()) { 445 LOG.debug("Creating new empty file: {}", target); 446 FileUtil.createNewFile(target); 447 } else if (endpoint.getFileExist() == GenericFileExist.Override) { 448 LOG.debug("Truncating existing file: {}", target); 449 FileChannel out = new FileOutputStream(target).getChannel(); 450 try { 451 out.truncate(0); 452 } finally { 453 IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); 454 } 455 } 456 } 457 458 /** 459 * Creates and prepares the output file channel. Will position itself in correct position if the file is writable 460 * eg. it should append or override any existing content. 461 */ 462 private FileChannel prepareOutputFileChannel(File target) throws IOException { 463 if (endpoint.getFileExist() == GenericFileExist.Append) { 464 FileChannel out = new RandomAccessFile(target, "rw").getChannel(); 465 return out.position(out.size()); 466 } 467 return new FileOutputStream(target).getChannel(); 468 } 469}