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