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