/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.component.file; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.io.Reader; import java.io.Writer; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.Date; import java.util.List; import java.util.Set; import org.apache.camel.Exchange; import org.apache.camel.InvalidPayloadException; import org.apache.camel.WrappedFile; import org.apache.camel.converter.IOConverter; import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * File operations for {@link java.io.File}. */ public class FileOperations implements GenericFileOperations<File> { private static final Logger LOG = LoggerFactory.getLogger(FileOperations.class); private FileEndpoint endpoint; public FileOperations() { } public FileOperations(FileEndpoint endpoint) { this.endpoint = endpoint; } public void setEndpoint(GenericFileEndpoint<File> endpoint) { this.endpoint = (FileEndpoint) endpoint; } public boolean deleteFile(String name) throws GenericFileOperationFailedException { File file = new File(name); return FileUtil.deleteFile(file); } public boolean renameFile(String from, String to) throws GenericFileOperationFailedException { boolean renamed = false; File file = new File(from); File target = new File(to); try { if (endpoint.isRenameUsingCopy()) { renamed = FileUtil.renameFileUsingCopy(file, target); } else { renamed = FileUtil.renameFile(file, target, endpoint.isCopyAndDeleteOnRenameFail()); } } catch (IOException e) { throw new GenericFileOperationFailedException("Error renaming file from " + from + " to " + to, e); } return renamed; } public boolean existsFile(String name) throws GenericFileOperationFailedException { File file = new File(name); return file.exists(); } protected boolean buildDirectory(File dir, Set<PosixFilePermission> permissions) { if (dir.exists()) { return true; } if (permissions == null || permissions.isEmpty()) { return dir.mkdirs(); } // create directory one part of a time and set permissions try { String[] parts = dir.getPath().split("\\" + File.separatorChar); File base = new File("."); for (String part : parts) { File subDir = new File(base, part); if (!subDir.exists()) { if (subDir.mkdir()) { if (LOG.isTraceEnabled()) { LOG.trace("Setting chmod: {} on directory: {} ", PosixFilePermissions.toString(permissions), subDir); } Files.setPosixFilePermissions(subDir.toPath(), permissions); } else { return false; } } base = new File(base, subDir.getName()); } } catch (IOException e) { throw new GenericFileOperationFailedException("Error setting chmod on directory: " + dir, e); } return true; } public boolean buildDirectory(String directory, boolean absolute) throws GenericFileOperationFailedException { ObjectHelper.notNull(endpoint, "endpoint"); // always create endpoint defined directory if (endpoint.isAutoCreate() && !endpoint.getFile().exists()) { LOG.trace("Building starting directory: {}", endpoint.getFile()); buildDirectory(endpoint.getFile(), endpoint.getDirectoryPermissions()); } if (ObjectHelper.isEmpty(directory)) { // no directory to build so return true to indicate ok return true; } File endpointPath = endpoint.getFile(); File target = new File(directory); File path; if (absolute) { // absolute path path = target; } else if (endpointPath.equals(target)) { // its just the root of the endpoint path path = endpointPath; } else { // relative after the endpoint path String afterRoot = ObjectHelper.after(directory, endpointPath.getPath() + File.separator); if (ObjectHelper.isNotEmpty(afterRoot)) { // dir is under the root path path = new File(endpoint.getFile(), afterRoot); } else { // dir is relative to the root path path = new File(endpoint.getFile(), directory); } } // We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time. synchronized (this) { if (path.isDirectory() && path.exists()) { // the directory already exists return true; } else { LOG.trace("Building directory: {}", path); return buildDirectory(path, endpoint.getDirectoryPermissions()); } } } public List<File> listFiles() throws GenericFileOperationFailedException { // noop return null; } public List<File> listFiles(String path) throws GenericFileOperationFailedException { // noop return null; } public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { // noop } public void changeToParentDirectory() throws GenericFileOperationFailedException { // noop } public String getCurrentDirectory() throws GenericFileOperationFailedException { // noop return null; } public boolean retrieveFile(String name, Exchange exchange) throws GenericFileOperationFailedException { // noop as we use type converters to read the body content for java.io.File return true; } @Override public void releaseRetreivedFileResources(Exchange exchange) throws GenericFileOperationFailedException { // noop as we used type converters to read the body content for java.io.File } public boolean storeFile(String fileName, Exchange exchange) throws GenericFileOperationFailedException { ObjectHelper.notNull(endpoint, "endpoint"); File file = new File(fileName); // if an existing file already exists what should we do? if (file.exists()) { if (endpoint.getFileExist() == GenericFileExist.Ignore) { // ignore but indicate that the file was written LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file); return true; } else if (endpoint.getFileExist() == GenericFileExist.Fail) { throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file."); } else if (endpoint.getFileExist() == GenericFileExist.Move) { // move any existing file first doMoveExistingFile(fileName); } } // Do an explicit test for a null body and decide what to do if (exchange.getIn().getBody() == null) { if (endpoint.isAllowNullBody()) { LOG.trace("Writing empty file."); try { writeFileEmptyBody(file); return true; } catch (IOException e) { throw new GenericFileOperationFailedException("Cannot store file: " + file, e); } } else { throw new GenericFileOperationFailedException("Cannot write null body to file: " + file); } } // we can write the file by 3 different techniques // 1. write file to file // 2. rename a file from a local work path // 3. write stream to file try { // is there an explicit charset configured we must write the file as String charset = endpoint.getCharset(); // we can optimize and use file based if no charset must be used, and the input body is a file File source = null; boolean fileBased = false; if (charset == null) { // if no charset, then we can try using file directly (optimized) Object body = exchange.getIn().getBody(); if (body instanceof WrappedFile) { body = ((WrappedFile<?>) body).getFile(); } if (body instanceof File) { source = (File) body; fileBased = true; } } if (fileBased) { // okay we know the body is a file based // so try to see if we can optimize by renaming the local work path file instead of doing // a full file to file copy, as the local work copy is to be deleted afterwards anyway // local work path File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class); if (local != null && local.exists()) { boolean renamed = writeFileByLocalWorkPath(local, file); if (renamed) { // try to keep last modified timestamp if configured to do so keepLastModified(exchange, file); // set permissions if the chmod option was set if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { Set<PosixFilePermission> permissions = endpoint.getPermissions(); if (!permissions.isEmpty()) { if (LOG.isTraceEnabled()) { LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file); } Files.setPosixFilePermissions(file.toPath(), permissions); } } // clear header as we have renamed the file exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null); // return as the operation is complete, we just renamed the local work file // to the target. return true; } } else if (source != null && source.exists()) { // no there is no local work file so use file to file copy if the source exists writeFileByFile(source, file); // try to keep last modified timestamp if configured to do so keepLastModified(exchange, file); // set permissions if the chmod option was set if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { Set<PosixFilePermission> permissions = endpoint.getPermissions(); if (!permissions.isEmpty()) { if (LOG.isTraceEnabled()) { LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file); } Files.setPosixFilePermissions(file.toPath(), permissions); } } return true; } } if (charset != null) { // charset configured so we must use a reader so we can write with encoding Reader in = exchange.getContext().getTypeConverter().tryConvertTo(Reader.class, exchange, exchange.getIn().getBody()); if (in == null) { // okay no direct reader conversion, so use an input stream (which a lot can be converted as) InputStream is = exchange.getIn().getMandatoryBody(InputStream.class); in = new InputStreamReader(is); } // buffer the reader in = IOHelper.buffered(in); writeFileByReaderWithCharset(in, file, charset); } else { // fallback and use stream based InputStream in = exchange.getIn().getMandatoryBody(InputStream.class); writeFileByStream(in, file); } // try to keep last modified timestamp if configured to do so keepLastModified(exchange, file); // set permissions if the chmod option was set if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { Set<PosixFilePermission> permissions = endpoint.getPermissions(); if (!permissions.isEmpty()) { if (LOG.isTraceEnabled()) { LOG.trace("Setting chmod: {} on file: {} ", PosixFilePermissions.toString(permissions), file); } Files.setPosixFilePermissions(file.toPath(), permissions); } } return true; } catch (IOException e) { throw new GenericFileOperationFailedException("Cannot store file: " + file, e); } catch (InvalidPayloadException e) { throw new GenericFileOperationFailedException("Cannot store file: " + file, e); } } /** * Moves any existing file due fileExists=Move is in use. */ private void doMoveExistingFile(String fileName) throws GenericFileOperationFailedException { // need to evaluate using a dummy and simulate the file first, to have access to all the file attributes // create a dummy exchange as Exchange is needed for expression evaluation // we support only the following 3 tokens. Exchange dummy = endpoint.createExchange(); String parent = FileUtil.onlyPath(fileName); String onlyName = FileUtil.stripPath(fileName); dummy.getIn().setHeader(Exchange.FILE_NAME, fileName); dummy.getIn().setHeader(Exchange.FILE_NAME_ONLY, onlyName); dummy.getIn().setHeader(Exchange.FILE_PARENT, parent); String to = endpoint.getMoveExisting().evaluate(dummy, String.class); // we must normalize it (to avoid having both \ and / in the name which confuses java.io.File) to = FileUtil.normalizePath(to); if (ObjectHelper.isEmpty(to)) { throw new GenericFileOperationFailedException("moveExisting evaluated as empty String, cannot move existing file: " + fileName); } // ensure any paths is created before we rename as the renamed file may be in a different path (which may be non exiting) // use java.io.File to compute the file path File toFile = new File(to); String directory = toFile.getParent(); boolean absolute = FileUtil.isAbsolute(toFile); if (directory != null) { if (!buildDirectory(directory, absolute)) { LOG.debug("Cannot build directory [{}] (could be because of denied permissions)", directory); } } // deal if there already exists a file if (existsFile(to)) { if (endpoint.isEagerDeleteTargetFile()) { LOG.trace("Deleting existing file: {}", to); if (!deleteFile(to)) { throw new GenericFileOperationFailedException("Cannot delete file: " + to); } } else { throw new GenericFileOperationFailedException("Cannot moved existing file from: " + fileName + " to: " + to + " as there already exists a file: " + to); } } LOG.trace("Moving existing file: {} to: {}", fileName, to); if (!renameFile(fileName, to)) { throw new GenericFileOperationFailedException("Cannot rename file from: " + fileName + " to: " + to); } } private void keepLastModified(Exchange exchange, File file) { if (endpoint.isKeepLastModified()) { Long last; Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class); if (date != null) { last = date.getTime(); } else { // fallback and try a long last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class); } if (last != null) { boolean result = file.setLastModified(last); if (LOG.isTraceEnabled()) { LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", new Object[]{last, file, result}); } } } } private boolean writeFileByLocalWorkPath(File source, File file) throws IOException { LOG.trace("Using local work file being renamed from: {} to: {}", source, file); return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail()); } private void writeFileByFile(File source, File target) throws IOException { FileChannel in = new FileInputStream(source).getChannel(); FileChannel out = null; try { out = prepareOutputFileChannel(target); LOG.debug("Using FileChannel to write file: {}", target); long size = in.size(); long position = 0; while (position < size) { position += in.transferTo(position, endpoint.getBufferSize(), out); } } finally { IOHelper.close(in, source.getName(), LOG); IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); } } private void writeFileByStream(InputStream in, File target) throws IOException { FileChannel out = null; try { out = prepareOutputFileChannel(target); LOG.debug("Using InputStream to write file: {}", target); int size = endpoint.getBufferSize(); byte[] buffer = new byte[size]; ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { if (bytesRead < size) { byteBuffer.limit(bytesRead); } out.write(byteBuffer); byteBuffer.clear(); } } finally { IOHelper.close(in, target.getName(), LOG); IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); } } private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException { boolean append = endpoint.getFileExist() == GenericFileExist.Append; FileOutputStream os = new FileOutputStream(target, append); Writer out = IOConverter.toWriter(os, charset); try { LOG.debug("Using Reader to write file: {} with charset: {}", target, charset); int size = endpoint.getBufferSize(); IOHelper.copy(in, out, size); } finally { IOHelper.close(in, target.getName(), LOG); IOHelper.close(out, os, target.getName(), LOG, endpoint.isForceWrites()); } } /** * Creates a new file if the file doesn't exist. * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated */ private void writeFileEmptyBody(File target) throws IOException { if (!target.exists()) { LOG.debug("Creating new empty file: {}", target); FileUtil.createNewFile(target); } else if (endpoint.getFileExist() == GenericFileExist.Override) { LOG.debug("Truncating existing file: {}", target); FileChannel out = new FileOutputStream(target).getChannel(); try { out.truncate(0); } finally { IOHelper.close(out, target.getName(), LOG, endpoint.isForceWrites()); } } } /** * Creates and prepares the output file channel. Will position itself in correct position if the file is writable * eg. it should append or override any existing content. */ private FileChannel prepareOutputFileChannel(File target) throws IOException { if (endpoint.getFileExist() == GenericFileExist.Append) { FileChannel out = new RandomAccessFile(target, "rw").getChannel(); return out.position(out.size()); } return new FileOutputStream(target).getChannel(); } }