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