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