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.FileNotFoundException;
021import java.nio.file.Files;
022import java.nio.file.attribute.PosixFilePermission;
023import java.util.Arrays;
024import java.util.HashSet;
025import java.util.Set;
026
027import org.apache.camel.Component;
028import org.apache.camel.Exchange;
029import org.apache.camel.Message;
030import org.apache.camel.PollingConsumer;
031import org.apache.camel.Processor;
032import org.apache.camel.component.file.strategy.FileMoveExistingStrategy;
033import org.apache.camel.processor.idempotent.MemoryIdempotentRepository;
034import org.apache.camel.spi.Metadata;
035import org.apache.camel.spi.UriEndpoint;
036import org.apache.camel.spi.UriParam;
037import org.apache.camel.spi.UriPath;
038import org.apache.camel.util.FileUtil;
039import org.apache.camel.util.ObjectHelper;
040
041/**
042 * The file component is used for reading or writing files.
043 */
044@UriEndpoint(firstVersion = "1.0.0", scheme = "file", title = "File", syntax = "file:directoryName", consumerClass = FileConsumer.class, label = "core,file")
045public class FileEndpoint extends GenericFileEndpoint<File> {
046
047    private static final Integer CHMOD_WRITE_MASK = 02;
048    private static final Integer CHMOD_READ_MASK = 04;
049    private static final Integer CHMOD_EXECUTE_MASK = 01;
050
051    private final FileOperations operations = new FileOperations(this);
052
053    @UriPath(name = "directoryName") @Metadata(required = "true")
054    private File file;
055    @UriParam(label = "advanced", defaultValue = "true")
056    private boolean copyAndDeleteOnRenameFail = true;
057    @UriParam(label = "advanced")
058    private boolean renameUsingCopy;
059    @UriParam(label = "producer,advanced", defaultValue = "true")
060    private boolean forceWrites = true;
061    @UriParam(label = "consumer,advanced")
062    private boolean probeContentType;
063    @UriParam(label = "consumer,advanced")
064    private String extendedAttributes;
065    @UriParam(label = "producer,advanced")
066    private String chmod;
067    @UriParam(label = "producer,advanced")
068    private String chmodDirectory;
069
070    public FileEndpoint() {
071    }
072
073    public FileEndpoint(String endpointUri, Component component) {
074        super(endpointUri, component);
075    }
076
077    public FileConsumer createConsumer(Processor processor) throws Exception {
078        ObjectHelper.notNull(operations, "operations");
079        ObjectHelper.notNull(file, "file");
080
081        // auto create starting directory if needed
082        if (!file.exists() && !file.isDirectory()) {
083            if (isAutoCreate()) {
084                log.debug("Creating non existing starting directory: {}", file);
085                boolean absolute = FileUtil.isAbsolute(file);
086                boolean created = operations.buildDirectory(file.getPath(), absolute);
087                if (!created) {
088                    log.warn("Cannot auto create starting directory: {}", file);
089                }
090            } else if (isStartingDirectoryMustExist()) {
091                throw new FileNotFoundException("Starting directory does not exist: " + file);
092            }
093        }
094
095        FileConsumer result = newFileConsumer(processor, operations);
096
097        if (isDelete() && getMove() != null) {
098            throw new IllegalArgumentException("You cannot set both delete=true and move options");
099        }
100
101        // if noop=true then idempotent should also be configured
102        if (isNoop() && !isIdempotentSet()) {
103            log.info("Endpoint is configured with noop=true so forcing endpoint to be idempotent as well");
104            setIdempotent(true);
105        }
106
107        // if idempotent and no repository set then create a default one
108        if (isIdempotentSet() && isIdempotent() && idempotentRepository == null) {
109            log.info("Using default memory based idempotent repository with cache max size: {}", DEFAULT_IDEMPOTENT_CACHE_SIZE);
110            idempotentRepository = MemoryIdempotentRepository.memoryIdempotentRepository(DEFAULT_IDEMPOTENT_CACHE_SIZE);
111        }
112
113        if (ObjectHelper.isNotEmpty(getReadLock())) {
114            // check if its a valid
115            String valid = "none,markerFile,fileLock,rename,changed,idempotent,idempotent-changed,idempotent-rename";
116            String[] arr = valid.split(",");
117            boolean matched = Arrays.stream(arr).anyMatch(n -> n.equals(getReadLock()));
118            if (!matched) {
119                throw new IllegalArgumentException("ReadLock invalid: " + getReadLock() + ", must be one of: " + valid);
120            }
121        }
122
123        if ("fileLock".equals(readLock) && FileUtil.isWindows()) {
124            throw new IllegalArgumentException("The readLock=fileLock option is not supported on Windows");
125        }
126
127        // set max messages per poll
128        result.setMaxMessagesPerPoll(getMaxMessagesPerPoll());
129        result.setEagerLimitMaxMessagesPerPoll(isEagerMaxMessagesPerPoll());
130
131        configureConsumer(result);
132        return result;
133    }
134
135    @Override
136    public PollingConsumer createPollingConsumer() throws Exception {
137        ObjectHelper.notNull(operations, "operations");
138        ObjectHelper.notNull(file, "file");
139
140        if (log.isDebugEnabled()) {
141            log.debug("Creating GenericFilePollingConsumer with queueSize: {} blockWhenFull: {} blockTimeout: {}",
142                getPollingConsumerQueueSize(), isPollingConsumerBlockWhenFull(), getPollingConsumerBlockTimeout());
143        }
144        GenericFilePollingConsumer result = new GenericFilePollingConsumer(this);
145        // should not call configurePollingConsumer when its GenericFilePollingConsumer
146        result.setBlockWhenFull(isPollingConsumerBlockWhenFull());
147        result.setBlockTimeout(getPollingConsumerBlockTimeout());
148
149        return result;
150    }
151
152    public GenericFileProducer<File> createProducer() throws Exception {
153        ObjectHelper.notNull(operations, "operations");
154
155        // you cannot use temp file and file exists append
156        if (getFileExist() == GenericFileExist.Append && ((getTempPrefix() != null) || (getTempFileName() != null))) {
157            throw new IllegalArgumentException("You cannot set both fileExist=Append and tempPrefix/tempFileName options");
158        }
159
160        // ensure fileExist and moveExisting is configured correctly if in use
161        if (getFileExist() == GenericFileExist.Move && getMoveExisting() == null) {
162            throw new IllegalArgumentException("You must configure moveExisting option when fileExist=Move");
163        } else if (getMoveExisting() != null && getFileExist() != GenericFileExist.Move) {
164            throw new IllegalArgumentException("You must configure fileExist=Move when moveExisting has been set");
165        }
166        if (this.getMoveExistingFileStrategy() == null) {
167            this.setMoveExistingFileStrategy(createDefaultMoveExistingFileStrategy());
168        }
169        return new GenericFileProducer<>(this, operations);
170    }
171
172    public Exchange createExchange(GenericFile<File> file) {
173        Exchange exchange = createExchange();
174        if (file != null) {
175            file.bindToExchange(exchange, probeContentType);
176        }
177        return exchange;
178    }
179
180    /**
181     * Strategy to create a new {@link FileConsumer}
182     *
183     * @param processor  the given processor
184     * @param operations file operations
185     * @return the created consumer
186     */
187    protected FileConsumer newFileConsumer(Processor processor, GenericFileOperations<File> operations) {
188        return new FileConsumer(this, processor, operations, processStrategy != null ? processStrategy : createGenericFileStrategy());
189    }
190
191    /**
192     * Default Existing File Move Strategy
193     * @return the default implementation for file component
194     */
195    private FileMoveExistingStrategy createDefaultMoveExistingFileStrategy() {
196        return new GenericFileDefaultMoveExistingFileStrategy();
197    }
198
199    public File getFile() {
200        return file;
201    }
202
203    /**
204     * The starting directory
205     */
206    public void setFile(File file) {
207        this.file = file;
208        // update configuration as well
209        getConfiguration().setDirectory(FileUtil.isAbsolute(file) ? file.getAbsolutePath() : file.getPath());
210    }
211
212    @Override
213    public String getScheme() {
214        return "file";
215    }
216
217    @Override
218    protected String createEndpointUri() {
219        return getFile().toURI().toString();
220    }
221
222    @Override
223    public char getFileSeparator() {       
224        return File.separatorChar;
225    }
226
227    @Override
228    public boolean isAbsolute(String name) {
229        // relative or absolute path?
230        return FileUtil.isAbsolute(new File(name));
231    }
232
233    public boolean isCopyAndDeleteOnRenameFail() {
234        return copyAndDeleteOnRenameFail;
235    }
236
237    /**
238     * Whether to fallback and do a copy and delete file, in case the file could not be renamed directly. This option is not available for the FTP component.
239     */
240    public void setCopyAndDeleteOnRenameFail(boolean copyAndDeleteOnRenameFail) {
241        this.copyAndDeleteOnRenameFail = copyAndDeleteOnRenameFail;
242    }
243
244    public boolean isRenameUsingCopy() {
245        return renameUsingCopy;
246    }
247
248    /**
249     * Perform rename operations using a copy and delete strategy.
250     * This is primarily used in environments where the regular rename operation is unreliable (e.g. across different file systems or networks).
251     * This option takes precedence over the copyAndDeleteOnRenameFail parameter that will automatically fall back to the copy and delete strategy,
252     * but only after additional delays.
253     */
254    public void setRenameUsingCopy(boolean renameUsingCopy) {
255        this.renameUsingCopy = renameUsingCopy;
256    }
257
258    public boolean isForceWrites() {
259        return forceWrites;
260    }
261
262    /**
263     * Whether to force syncing writes to the file system.
264     * You can turn this off if you do not want this level of guarantee, for example if writing to logs / audit logs etc; this would yield better performance.
265     */
266    public void setForceWrites(boolean forceWrites) {
267        this.forceWrites = forceWrites;
268    }
269
270    public boolean isProbeContentType() {
271        return probeContentType;
272    }
273
274    /**
275     * Whether to enable probing of the content type. If enable then the consumer uses {@link Files#probeContentType(java.nio.file.Path)} to
276     * determine the content-type of the file, and store that as a header with key {@link Exchange#FILE_CONTENT_TYPE} on the {@link Message}.
277     */
278    public void setProbeContentType(boolean probeContentType) {
279        this.probeContentType = probeContentType;
280    }
281
282    public String getExtendedAttributes() {
283        return extendedAttributes;
284    }
285
286    /**
287     * To define which file attributes of interest. Like posix:permissions,posix:owner,basic:lastAccessTime,
288     * it supports basic wildcard like posix:*, basic:lastAccessTime
289     */
290    public void setExtendedAttributes(String extendedAttributes) {
291        this.extendedAttributes = extendedAttributes;
292    }
293
294    /**
295     * Chmod value must be between 000 and 777; If there is a leading digit like in 0755 we will ignore it.
296     */
297    public boolean chmodPermissionsAreValid(String chmod) {
298        if (chmod == null || chmod.length() < 3 || chmod.length() > 4) {
299            return false;
300        }
301        String permissionsString = chmod.trim().substring(chmod.length() - 3);  // if 4 digits chop off leading one
302        for (int i = 0; i < permissionsString.length(); i++) {
303            Character c = permissionsString.charAt(i);
304            if (!Character.isDigit(c) || Integer.parseInt(c.toString()) > 7) {
305                return false;
306            }
307        }
308        return true;
309    }
310
311    public Set<PosixFilePermission> getPermissions() {
312        Set<PosixFilePermission> permissions = new HashSet<>();
313        if (ObjectHelper.isEmpty(chmod)) {
314            return permissions;
315        }
316
317        String chmodString = chmod.substring(chmod.length() - 3);  // if 4 digits chop off leading one
318
319        Integer ownerValue = Integer.parseInt(chmodString.substring(0, 1));
320        Integer groupValue = Integer.parseInt(chmodString.substring(1, 2));
321        Integer othersValue = Integer.parseInt(chmodString.substring(2, 3));
322
323        if ((ownerValue & CHMOD_WRITE_MASK) > 0) {
324            permissions.add(PosixFilePermission.OWNER_WRITE);
325        }
326        if ((ownerValue & CHMOD_READ_MASK) > 0) {
327            permissions.add(PosixFilePermission.OWNER_READ);
328        }
329        if ((ownerValue & CHMOD_EXECUTE_MASK) > 0) {
330            permissions.add(PosixFilePermission.OWNER_EXECUTE);
331        }
332
333        if ((groupValue & CHMOD_WRITE_MASK) > 0) {
334            permissions.add(PosixFilePermission.GROUP_WRITE);
335        }
336        if ((groupValue & CHMOD_READ_MASK) > 0) {
337            permissions.add(PosixFilePermission.GROUP_READ);
338        }
339        if ((groupValue & CHMOD_EXECUTE_MASK) > 0) {
340            permissions.add(PosixFilePermission.GROUP_EXECUTE);
341        }
342
343        if ((othersValue & CHMOD_WRITE_MASK) > 0) {
344            permissions.add(PosixFilePermission.OTHERS_WRITE);
345        }
346        if ((othersValue & CHMOD_READ_MASK) > 0) {
347            permissions.add(PosixFilePermission.OTHERS_READ);
348        }
349        if ((othersValue & CHMOD_EXECUTE_MASK) > 0) {
350            permissions.add(PosixFilePermission.OTHERS_EXECUTE);
351        }
352
353        return permissions;
354    }
355
356    public String getChmod() {
357        return chmod;
358    }
359
360    /**
361     * Specify the file permissions which is sent by the producer, the chmod value must be between 000 and 777;
362     * If there is a leading digit like in 0755 we will ignore it.
363     */
364    public void setChmod(String chmod) throws Exception {
365        if (ObjectHelper.isNotEmpty(chmod) && chmodPermissionsAreValid(chmod)) {
366            this.chmod = chmod.trim();
367        } else {
368            throw new IllegalArgumentException("chmod option [" + chmod + "] is not valid");
369        }
370    }
371
372    public Set<PosixFilePermission> getDirectoryPermissions() {
373        Set<PosixFilePermission> permissions = new HashSet<>();
374        if (ObjectHelper.isEmpty(chmodDirectory)) {
375            return permissions;
376        }
377
378        String chmodString = chmodDirectory.substring(chmodDirectory.length() - 3);  // if 4 digits chop off leading one
379
380        Integer ownerValue = Integer.parseInt(chmodString.substring(0, 1));
381        Integer groupValue = Integer.parseInt(chmodString.substring(1, 2));
382        Integer othersValue = Integer.parseInt(chmodString.substring(2, 3));
383
384        if ((ownerValue & CHMOD_WRITE_MASK) > 0) {
385            permissions.add(PosixFilePermission.OWNER_WRITE);
386        }
387        if ((ownerValue & CHMOD_READ_MASK) > 0) {
388            permissions.add(PosixFilePermission.OWNER_READ);
389        }
390        if ((ownerValue & CHMOD_EXECUTE_MASK) > 0) {
391            permissions.add(PosixFilePermission.OWNER_EXECUTE);
392        }
393
394        if ((groupValue & CHMOD_WRITE_MASK) > 0) {
395            permissions.add(PosixFilePermission.GROUP_WRITE);
396        }
397        if ((groupValue & CHMOD_READ_MASK) > 0) {
398            permissions.add(PosixFilePermission.GROUP_READ);
399        }
400        if ((groupValue & CHMOD_EXECUTE_MASK) > 0) {
401            permissions.add(PosixFilePermission.GROUP_EXECUTE);
402        }
403
404        if ((othersValue & CHMOD_WRITE_MASK) > 0) {
405            permissions.add(PosixFilePermission.OTHERS_WRITE);
406        }
407        if ((othersValue & CHMOD_READ_MASK) > 0) {
408            permissions.add(PosixFilePermission.OTHERS_READ);
409        }
410        if ((othersValue & CHMOD_EXECUTE_MASK) > 0) {
411            permissions.add(PosixFilePermission.OTHERS_EXECUTE);
412        }
413
414        return permissions;
415    }
416
417    public String getChmodDirectory() {
418        return chmodDirectory;
419    }
420
421    /**
422     * Specify the directory permissions used when the producer creates missing directories, the chmod value must be between 000 and 777;
423     * If there is a leading digit like in 0755 we will ignore it.
424     */
425    public void setChmodDirectory(String chmodDirectory) throws Exception {
426        if (ObjectHelper.isNotEmpty(chmodDirectory) && chmodPermissionsAreValid(chmodDirectory)) {
427            this.chmodDirectory = chmodDirectory.trim();
428        } else {
429            throw new IllegalArgumentException("chmodDirectory option [" + chmodDirectory + "] is not valid");
430        }
431    }
432
433}