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