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