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, long size) 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 releaseRetrievedFileResources(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, long size) 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 this.endpoint.getMoveExistingFileStrategy().moveExistingFile(endpoint, this, 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 private void keepLastModified(Exchange exchange, File file) { 359 if (endpoint.isKeepLastModified()) { 360 Long last; 361 Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class); 362 if (date != null) { 363 last = date.getTime(); 364 } else { 365 // fallback and try a long 366 last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class); 367 } 368 if (last != null) { 369 boolean result = file.setLastModified(last); 370 if (LOG.isTraceEnabled()) { 371 LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", last, file, result); 372 } 373 } 374 } 375 } 376 377 private boolean writeFileByLocalWorkPath(File source, File file) throws IOException { 378 LOG.trace("Using local work file being renamed from: {} to: {}", source, file); 379 return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail()); 380 } 381 382 private void writeFileByFile(File source, File target) throws IOException { 383 Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); 384 } 385 386 private void writeFileByStream(InputStream in, File target) throws IOException { 387 try (SeekableByteChannel out = prepareOutputFileChannel(target)) { 388 389 LOG.debug("Using InputStream to write file: {}", target); 390 int size = endpoint.getBufferSize(); 391 byte[] buffer = new byte[size]; 392 ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); 393 int bytesRead; 394 while ((bytesRead = in.read(buffer)) != -1) { 395 if (bytesRead < size) { 396 byteBuffer.limit(bytesRead); 397 } 398 out.write(byteBuffer); 399 byteBuffer.clear(); 400 } 401 } finally { 402 IOHelper.close(in, target.getName(), LOG); 403 } 404 } 405 406 private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException { 407 boolean append = endpoint.getFileExist() == GenericFileExist.Append; 408 try (Writer out = Files.newBufferedWriter(target.toPath(), Charset.forName(charset), 409 StandardOpenOption.WRITE, 410 append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING, 411 StandardOpenOption.CREATE)) { 412 LOG.debug("Using Reader to write file: {} with charset: {}", target, charset); 413 int size = endpoint.getBufferSize(); 414 IOHelper.copy(in, out, size); 415 } finally { 416 IOHelper.close(in, target.getName(), LOG); 417 } 418 } 419 420 /** 421 * Creates a new file if the file doesn't exist. 422 * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated 423 */ 424 private void writeFileEmptyBody(File target) throws IOException { 425 if (!target.exists()) { 426 LOG.debug("Creating new empty file: {}", target); 427 FileUtil.createNewFile(target); 428 } else if (endpoint.getFileExist() == GenericFileExist.Override) { 429 LOG.debug("Truncating existing file: {}", target); 430 try (SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { 431 //nothing to write 432 } 433 } 434 } 435 436 /** 437 * Creates and prepares the output file channel. Will position itself in correct position if the file is writable 438 * eg. it should append or override any existing content. 439 */ 440 private SeekableByteChannel prepareOutputFileChannel(File target) throws IOException { 441 if (endpoint.getFileExist() == GenericFileExist.Append) { 442 SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); 443 return out.position(out.size()); 444 } 445 return Files.newByteChannel(target.toPath(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); 446 } 447}