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 // check if directory is a path 152 boolean isPath = directory.contains("/") || directory.contains("\\"); 153 154 File path; 155 if (absolute) { 156 // absolute path 157 path = target; 158 } else if (endpointPath.equals(target)) { 159 // its just the root of the endpoint path 160 path = endpointPath; 161 } else if (isPath) { 162 // relative after the endpoint path 163 String afterRoot = StringHelper.after(directory, endpointPath.getPath() + File.separator); 164 if (ObjectHelper.isNotEmpty(afterRoot)) { 165 // dir is under the root path 166 path = new File(endpoint.getFile(), afterRoot); 167 } else { 168 // dir path is relative to the root path 169 path = new File(directory); 170 } 171 } else { 172 // dir is a child of the root path 173 path = new File(endpoint.getFile(), directory); 174 } 175 176 // We need to make sure that this is thread-safe and only one thread tries to create the path directory at the same time. 177 synchronized (this) { 178 if (path.isDirectory() && path.exists()) { 179 // the directory already exists 180 return true; 181 } else { 182 LOG.trace("Building directory: {}", path); 183 return buildDirectory(path, endpoint.getDirectoryPermissions(), absolute); 184 } 185 } 186 } 187 188 public List<File> listFiles() throws GenericFileOperationFailedException { 189 // noop 190 return null; 191 } 192 193 public List<File> listFiles(String path) throws GenericFileOperationFailedException { 194 // noop 195 return null; 196 } 197 198 public void changeCurrentDirectory(String path) throws GenericFileOperationFailedException { 199 // noop 200 } 201 202 public void changeToParentDirectory() throws GenericFileOperationFailedException { 203 // noop 204 } 205 206 public String getCurrentDirectory() throws GenericFileOperationFailedException { 207 // noop 208 return null; 209 } 210 211 public boolean retrieveFile(String name, Exchange exchange, long size) throws GenericFileOperationFailedException { 212 // noop as we use type converters to read the body content for java.io.File 213 return true; 214 } 215 216 @Override 217 public void releaseRetrievedFileResources(Exchange exchange) throws GenericFileOperationFailedException { 218 // noop as we used type converters to read the body content for java.io.File 219 } 220 221 public boolean storeFile(String fileName, Exchange exchange, long size) throws GenericFileOperationFailedException { 222 ObjectHelper.notNull(endpoint, "endpoint"); 223 224 File file = new File(fileName); 225 226 // if an existing file already exists what should we do? 227 if (file.exists()) { 228 if (endpoint.getFileExist() == GenericFileExist.Ignore) { 229 // ignore but indicate that the file was written 230 LOG.trace("An existing file already exists: {}. Ignore and do not override it.", file); 231 return true; 232 } else if (endpoint.getFileExist() == GenericFileExist.Fail) { 233 throw new GenericFileOperationFailedException("File already exist: " + file + ". Cannot write new file."); 234 } else if (endpoint.getFileExist() == GenericFileExist.Move) { 235 // move any existing file first 236 this.endpoint.getMoveExistingFileStrategy().moveExistingFile(endpoint, this, fileName); 237 } 238 } 239 240 // Do an explicit test for a null body and decide what to do 241 if (exchange.getIn().getBody() == null) { 242 if (endpoint.isAllowNullBody()) { 243 LOG.trace("Writing empty file."); 244 try { 245 writeFileEmptyBody(file); 246 return true; 247 } catch (IOException e) { 248 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 249 } 250 } else { 251 throw new GenericFileOperationFailedException("Cannot write null body to file: " + file); 252 } 253 } 254 255 // we can write the file by 3 different techniques 256 // 1. write file to file 257 // 2. rename a file from a local work path 258 // 3. write stream to file 259 try { 260 261 // is there an explicit charset configured we must write the file as 262 String charset = endpoint.getCharset(); 263 264 // we can optimize and use file based if no charset must be used, and the input body is a file 265 // however optimization cannot be applied when content should be appended to target file 266 File source = null; 267 boolean fileBased = false; 268 if (charset == null && endpoint.getFileExist() != GenericFileExist.Append) { 269 // if no charset and not in appending mode, then we can try using file directly (optimized) 270 Object body = exchange.getIn().getBody(); 271 if (body instanceof WrappedFile) { 272 body = ((WrappedFile<?>) body).getFile(); 273 } 274 if (body instanceof File) { 275 source = (File) body; 276 fileBased = true; 277 } 278 } 279 280 if (fileBased) { 281 // okay we know the body is a file based 282 283 // so try to see if we can optimize by renaming the local work path file instead of doing 284 // a full file to file copy, as the local work copy is to be deleted afterwards anyway 285 // local work path 286 File local = exchange.getIn().getHeader(Exchange.FILE_LOCAL_WORK_PATH, File.class); 287 if (local != null && local.exists()) { 288 boolean renamed = writeFileByLocalWorkPath(local, file); 289 if (renamed) { 290 // try to keep last modified timestamp if configured to do so 291 keepLastModified(exchange, file); 292 // set permissions if the chmod option was set 293 if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { 294 Set<PosixFilePermission> permissions = endpoint.getPermissions(); 295 if (!permissions.isEmpty()) { 296 if (LOG.isTraceEnabled()) { 297 LOG.trace("Setting chmod: {} on file: {}", PosixFilePermissions.toString(permissions), file); 298 } 299 Files.setPosixFilePermissions(file.toPath(), permissions); 300 } 301 } 302 // clear header as we have renamed the file 303 exchange.getIn().setHeader(Exchange.FILE_LOCAL_WORK_PATH, null); 304 // return as the operation is complete, we just renamed the local work file 305 // to the target. 306 return true; 307 } 308 } else if (source != null && source.exists()) { 309 // no there is no local work file so use file to file copy if the source exists 310 writeFileByFile(source, file); 311 // try to keep last modified timestamp if configured to do so 312 keepLastModified(exchange, file); 313 // set permissions if the chmod option was set 314 if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { 315 Set<PosixFilePermission> permissions = endpoint.getPermissions(); 316 if (!permissions.isEmpty()) { 317 if (LOG.isTraceEnabled()) { 318 LOG.trace("Setting chmod: {} on file: {}", PosixFilePermissions.toString(permissions), file); 319 } 320 Files.setPosixFilePermissions(file.toPath(), permissions); 321 } 322 } 323 return true; 324 } 325 } 326 327 if (charset != null) { 328 // charset configured so we must use a reader so we can write with encoding 329 Reader in = exchange.getContext().getTypeConverter().tryConvertTo(Reader.class, exchange, exchange.getIn().getBody()); 330 if (in == null) { 331 // okay no direct reader conversion, so use an input stream (which a lot can be converted as) 332 InputStream is = exchange.getIn().getMandatoryBody(InputStream.class); 333 in = new InputStreamReader(is); 334 } 335 // buffer the reader 336 in = IOHelper.buffered(in); 337 writeFileByReaderWithCharset(in, file, charset); 338 } else { 339 // fallback and use stream based 340 InputStream in = exchange.getIn().getMandatoryBody(InputStream.class); 341 writeFileByStream(in, file); 342 } 343 344 // try to keep last modified timestamp if configured to do so 345 keepLastModified(exchange, file); 346 // set permissions if the chmod option was set 347 if (ObjectHelper.isNotEmpty(endpoint.getChmod())) { 348 Set<PosixFilePermission> permissions = endpoint.getPermissions(); 349 if (!permissions.isEmpty()) { 350 if (LOG.isTraceEnabled()) { 351 LOG.trace("Setting chmod: {} on file: {}", PosixFilePermissions.toString(permissions), file); 352 } 353 Files.setPosixFilePermissions(file.toPath(), permissions); 354 } 355 } 356 357 return true; 358 } catch (IOException e) { 359 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 360 } catch (InvalidPayloadException e) { 361 throw new GenericFileOperationFailedException("Cannot store file: " + file, e); 362 } 363 } 364 365 private void keepLastModified(Exchange exchange, File file) { 366 if (endpoint.isKeepLastModified()) { 367 Long last; 368 Date date = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Date.class); 369 if (date != null) { 370 last = date.getTime(); 371 } else { 372 // fallback and try a long 373 last = exchange.getIn().getHeader(Exchange.FILE_LAST_MODIFIED, Long.class); 374 } 375 if (last != null) { 376 boolean result = file.setLastModified(last); 377 if (LOG.isTraceEnabled()) { 378 LOG.trace("Keeping last modified timestamp: {} on file: {} with result: {}", last, file, result); 379 } 380 } 381 } 382 } 383 384 private boolean writeFileByLocalWorkPath(File source, File file) throws IOException { 385 LOG.trace("Using local work file being renamed from: {} to: {}", source, file); 386 return FileUtil.renameFile(source, file, endpoint.isCopyAndDeleteOnRenameFail()); 387 } 388 389 private void writeFileByFile(File source, File target) throws IOException { 390 Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); 391 } 392 393 private void writeFileByStream(InputStream in, File target) throws IOException { 394 try (SeekableByteChannel out = prepareOutputFileChannel(target)) { 395 396 LOG.debug("Using InputStream to write file: {}", target); 397 int size = endpoint.getBufferSize(); 398 byte[] buffer = new byte[size]; 399 ByteBuffer byteBuffer = ByteBuffer.wrap(buffer); 400 int bytesRead; 401 while ((bytesRead = in.read(buffer)) != -1) { 402 if (bytesRead < size) { 403 byteBuffer.limit(bytesRead); 404 } 405 out.write(byteBuffer); 406 byteBuffer.clear(); 407 } 408 } finally { 409 IOHelper.close(in, target.getName(), LOG); 410 } 411 } 412 413 private void writeFileByReaderWithCharset(Reader in, File target, String charset) throws IOException { 414 boolean append = endpoint.getFileExist() == GenericFileExist.Append; 415 try (Writer out = Files.newBufferedWriter(target.toPath(), Charset.forName(charset), 416 StandardOpenOption.WRITE, 417 append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING, 418 StandardOpenOption.CREATE)) { 419 LOG.debug("Using Reader to write file: {} with charset: {}", target, charset); 420 int size = endpoint.getBufferSize(); 421 IOHelper.copy(in, out, size); 422 } finally { 423 IOHelper.close(in, target.getName(), LOG); 424 } 425 } 426 427 /** 428 * Creates a new file if the file doesn't exist. 429 * If the endpoint's existing file logic is set to 'Override' then the target file will be truncated 430 */ 431 private void writeFileEmptyBody(File target) throws IOException { 432 if (!target.exists()) { 433 LOG.debug("Creating new empty file: {}", target); 434 FileUtil.createNewFile(target); 435 } else if (endpoint.getFileExist() == GenericFileExist.Override) { 436 LOG.debug("Truncating existing file: {}", target); 437 try (SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { 438 //nothing to write 439 } 440 } 441 } 442 443 /** 444 * Creates and prepares the output file channel. Will position itself in correct position if the file is writable 445 * eg. it should append or override any existing content. 446 */ 447 private SeekableByteChannel prepareOutputFileChannel(File target) throws IOException { 448 if (endpoint.getFileExist() == GenericFileExist.Append) { 449 SeekableByteChannel out = Files.newByteChannel(target.toPath(), StandardOpenOption.CREATE, StandardOpenOption.APPEND); 450 return out.position(out.size()); 451 } 452 return Files.newByteChannel(target.toPath(), StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); 453 } 454}