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.util;
018
019import java.io.File;
020import java.io.IOException;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.nio.file.StandardCopyOption;
024import java.util.ArrayDeque;
025import java.util.Deque;
026import java.util.Iterator;
027import java.util.Locale;
028import java.util.Objects;
029
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033/**
034 * File utilities.
035 */
036public final class FileUtil {
037
038    public static final int BUFFER_SIZE = 128 * 1024;
039
040    private static final Logger LOG = LoggerFactory.getLogger(FileUtil.class);
041    private static final int RETRY_SLEEP_MILLIS = 10;
042    /**
043     * The System property key for the user directory.
044     */
045    private static final String USER_DIR_KEY = "user.dir";
046    private static final File USER_DIR = new File(System.getProperty(USER_DIR_KEY));
047    private static final boolean IS_WINDOWS = initWindowsOs();
048
049    private FileUtil() {
050        // Utils method
051    }
052
053    private static boolean initWindowsOs() {
054        // initialize once as System.getProperty is not fast
055        String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
056        return osName.contains("windows");
057    }
058
059    public static File getUserDir() {
060        return USER_DIR;
061    }
062
063    /**
064     * Normalizes the path to cater for Windows and other platforms
065     */
066    public static String normalizePath(String path) {
067        if (path == null) {
068            return null;
069        }
070
071        if (isWindows()) {
072            // special handling for Windows where we need to convert / to \\
073            return path.replace('/', '\\');
074        } else {
075            // for other systems make sure we use / as separators
076            return path.replace('\\', '/');
077        }
078    }
079
080    /**
081     * Returns true, if the OS is windows
082     */
083    public static boolean isWindows() {
084        return IS_WINDOWS;
085    }
086
087    @SuppressWarnings("ResultOfMethodCallIgnored")
088    public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException {
089        Objects.requireNonNull(parentDir);
090
091        if (suffix == null) {
092            suffix = ".tmp";
093        }
094        if (prefix == null) {
095            prefix = "camel";
096        } else if (prefix.length() < 3) {
097            prefix = prefix + "camel";
098        }
099
100        // create parent folder
101        parentDir.mkdirs();
102
103        return Files.createTempFile(parentDir.toPath(), prefix, suffix).toFile();
104    }
105
106    /**
107     * Strip any leading separators
108     */
109    public static String stripLeadingSeparator(String name) {
110        if (name == null) {
111            return null;
112        }
113        while (name.startsWith("/") || name.startsWith(File.separator)) {
114            name = name.substring(1);
115        }
116        return name;
117    }
118
119    /**
120     * Does the name start with a leading separator
121     */
122    public static boolean hasLeadingSeparator(String name) {
123        if (name == null) {
124            return false;
125        }
126        if (name.startsWith("/") || name.startsWith(File.separator)) {
127            return true;
128        }
129        return false;
130    }
131
132    /**
133     * Strip first leading separator
134     */
135    public static String stripFirstLeadingSeparator(String name) {
136        if (name == null) {
137            return null;
138        }
139        if (name.startsWith("/") || name.startsWith(File.separator)) {
140            name = name.substring(1);
141        }
142        return name;
143    }
144
145    /**
146     * Strip any trailing separators
147     */
148    public static String stripTrailingSeparator(String name) {
149        if (ObjectHelper.isEmpty(name)) {
150            return name;
151        }
152
153        String s = name;
154
155        // there must be some leading text, as we should only remove trailing separators
156        while (s.endsWith("/") || s.endsWith(File.separator)) {
157            s = s.substring(0, s.length() - 1);
158        }
159
160        // if the string is empty, that means there was only trailing slashes, and no leading text
161        // and so we should then return the original name as is
162        if (ObjectHelper.isEmpty(s)) {
163            return name;
164        } else {
165            // return without trailing slashes
166            return s;
167        }
168    }
169
170    /**
171     * Strips any leading paths
172     */
173    public static String stripPath(String name) {
174        if (name == null) {
175            return null;
176        }
177        int posUnix = name.lastIndexOf('/');
178        int posWin = name.lastIndexOf('\\');
179        int pos = Math.max(posUnix, posWin);
180
181        if (pos != -1) {
182            return name.substring(pos + 1);
183        }
184        return name;
185    }
186
187    public static String stripExt(String name) {
188        return stripExt(name, false);
189    }
190
191    public static String stripExt(String name, boolean singleMode) {
192        if (name == null) {
193            return null;
194        }
195
196        // the name may have a leading path
197        int posUnix = name.lastIndexOf('/');
198        int posWin = name.lastIndexOf('\\');
199        int pos = Math.max(posUnix, posWin);
200
201        if (pos > 0) {
202            String onlyName = name.substring(pos + 1);
203            int pos2 = singleMode ? onlyName.lastIndexOf('.') : onlyName.indexOf('.');
204            if (pos2 > 0) {
205                return name.substring(0, pos + pos2 + 1);
206            }
207        } else {
208            // if single ext mode, then only return last extension
209            int pos2 = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
210            if (pos2 > 0) {
211                return name.substring(0, pos2);
212            }
213        }
214
215        return name;
216    }
217
218    public static String onlyExt(String name) {
219        return onlyExt(name, false);
220    }
221
222    public static String onlyExt(String name, boolean singleMode) {
223        if (name == null) {
224            return null;
225        }
226        name = stripPath(name);
227
228        // extension is the first dot, as a file may have double extension such as .tar.gz
229        // if single ext mode, then only return last extension
230        int pos = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
231        if (pos != -1) {
232            return name.substring(pos + 1);
233        }
234        return null;
235    }
236
237    /**
238     * Returns only the leading path (returns <tt>null</tt> if no path)
239     */
240    public static String onlyPath(String name) {
241        if (name == null) {
242            return null;
243        }
244
245        int posUnix = name.lastIndexOf('/');
246        int posWin = name.lastIndexOf('\\');
247        int pos = Math.max(posUnix, posWin);
248
249        if (pos > 0) {
250            return name.substring(0, pos);
251        } else if (pos == 0) {
252            // name is in the root path, so extract the path as the first char
253            return name.substring(0, 1);
254        }
255        // no path in name
256        return null;
257    }
258
259    public static String onlyName(String name) {
260        return onlyName(name, false);
261    }
262
263    public static String onlyName(String name, boolean singleMode) {
264        name = FileUtil.stripPath(name);
265        name = FileUtil.stripExt(name, singleMode);
266
267        return name;
268    }
269
270    /**
271     * Compacts a path by stacking it and reducing <tt>..</tt>, and uses OS specific file separators (eg
272     * {@link java.io.File#separator}).
273     */
274    public static String compactPath(String path) {
275        return compactPath(path, String.valueOf(File.separatorChar));
276    }
277
278    /**
279     * Compacts a path by stacking it and reducing <tt>..</tt>, and uses the given separator.
280     */
281    public static String compactPath(String path, char separator) {
282        return compactPath(path, String.valueOf(separator));
283    }
284
285    /**
286     * Compacts a file path by stacking it and reducing <tt>..</tt>, and uses the given separator.
287     */
288    public static String compactPath(String path, String separator) {
289        if (path == null) {
290            return null;
291        }
292
293        if (path.startsWith("http:") || path.startsWith("https:")) {
294            return path;
295        }
296
297        // only normalize if contains a path separator
298        if (path.indexOf('/') == -1 && path.indexOf('\\') == -1) {
299            return path;
300        }
301
302        // need to normalize path before compacting
303        path = normalizePath(path);
304
305        // preserve scheme
306        String scheme = null;
307        if (hasScheme(path)) {
308            int pos = path.indexOf(':');
309            scheme = path.substring(0, pos);
310            path = path.substring(pos + 1);
311        }
312
313        // preserve ending slash if given in input path
314        boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\");
315
316        // preserve starting slash if given in input path
317        int cntSlashsAtStart = 0;
318        if (path.startsWith("/") || path.startsWith("\\")) {
319            cntSlashsAtStart++;
320            // for Windows, preserve up to 2 starting slashes, which is necessary for UNC paths.
321            if (isWindows() && path.length() > 1 && (path.charAt(1) == '/' || path.charAt(1) == '\\')) {
322                cntSlashsAtStart++;
323            }
324        }
325
326        Deque<String> stack = new ArrayDeque<>();
327
328        // separator can either be windows or unix style
329        String separatorRegex = "[\\\\/]";
330        String[] parts = path.split(separatorRegex);
331        for (String part : parts) {
332            if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) {
333                // only pop if there is a previous path, which is not a ".." path either
334                stack.pop();
335            } else if (!part.equals(".") && !part.isEmpty()) {
336                stack.push(part);
337            }
338            // else do nothing because we don't want a path like foo/./bar or foo//bar
339        }
340
341        // build path based on stack
342        StringBuilder sb = new StringBuilder(256);
343        if (scheme != null) {
344            sb.append(scheme);
345            sb.append(":");
346        }
347
348        sb.append(String.valueOf(separator).repeat(cntSlashsAtStart));
349
350        // now we build back using FIFO so need to use descending
351        for (Iterator<String> it = stack.descendingIterator(); it.hasNext();) {
352            sb.append(it.next());
353            if (it.hasNext()) {
354                sb.append(separator);
355            }
356        }
357
358        if (endsWithSlash && !stack.isEmpty()) {
359            sb.append(separator);
360        }
361
362        return sb.toString();
363    }
364
365    public static void removeDir(File d) {
366        String[] list = d.list();
367        if (list == null) {
368            list = new String[0];
369        }
370        for (String s : list) {
371            File f = new File(d, s);
372            if (f.isDirectory()) {
373                removeDir(f);
374            } else {
375                delete(f);
376            }
377        }
378        delete(d);
379    }
380
381    private static void delete(File f) {
382        try {
383            Files.delete(f.toPath());
384        } catch (IOException e) {
385            try {
386                Thread.sleep(RETRY_SLEEP_MILLIS);
387            } catch (InterruptedException ex) {
388                LOG.info("Interrupted while trying to delete file {}", f, e);
389                Thread.currentThread().interrupt();
390            }
391            try {
392                Files.delete(f.toPath());
393            } catch (IOException ex) {
394                f.deleteOnExit();
395            }
396        }
397    }
398
399    /**
400     * Renames a file.
401     *
402     * @param  from                      the from file
403     * @param  to                        the to file
404     * @param  copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
405     * @return                           <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
406     * @throws java.io.IOException       is thrown if error renaming file
407     */
408    public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
409        return renameFile(from.toPath(), to.toPath(), copyAndDeleteOnRenameFail);
410    }
411
412    /**
413     * Renames a file.
414     *
415     * @param  from                      the from file
416     * @param  to                        the to file
417     * @param  copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
418     * @return                           <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
419     * @throws java.io.IOException       is thrown if error renaming file
420     */
421    public static boolean renameFile(Path from, Path to, boolean copyAndDeleteOnRenameFail) throws IOException {
422        // do not try to rename non existing files
423        if (!Files.exists(from)) {
424            return false;
425        }
426
427        // some OS such as Windows can have problem doing rename IO operations so we may need to
428        // retry a couple of times to let it work
429        boolean renamed = false;
430        int count = 0;
431        while (!renamed && count < 3) {
432            if (LOG.isDebugEnabled() && count > 0) {
433                LOG.debug("Retrying attempt {} to rename file from: {} to: {}", count, from, to);
434            }
435
436            try {
437                Files.move(from, to, StandardCopyOption.ATOMIC_MOVE);
438                renamed = true;
439            } catch (IOException e) {
440                // failed
441            }
442            if (!renamed && count > 0) {
443                try {
444                    Thread.sleep(1000);
445                } catch (InterruptedException e) {
446                    LOG.info("Interrupted while trying to rename file from {} to {}", from, to, e);
447                    Thread.currentThread().interrupt();
448                }
449            }
450            count++;
451        }
452
453        // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
454        // for example if you move files between different file systems (linux -> windows etc.)
455        if (!renamed && copyAndDeleteOnRenameFail) {
456            // now do a copy and delete as all rename attempts failed
457            LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
458            renamed = renameFileUsingCopy(from, to);
459        }
460
461        if (LOG.isDebugEnabled() && count > 0) {
462            LOG.debug("Tried {} to rename file: {} to: {} with result: {}", count, from, to, renamed);
463        }
464        return renamed;
465    }
466
467    /**
468     * Rename file using copy and delete strategy. This is primarily used in environments where the regular rename
469     * operation is unreliable.
470     *
471     * @param  from        the file to be renamed
472     * @param  to          the new target file
473     * @return             <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt>
474     * @throws IOException If an I/O error occurs during copy or delete operations.
475     */
476    public static boolean renameFileUsingCopy(File from, File to) throws IOException {
477        return renameFileUsingCopy(from.toPath(), to.toPath());
478    }
479
480    /**
481     * Rename file using copy and delete strategy. This is primarily used in environments where the regular rename
482     * operation is unreliable.
483     *
484     * @param  from        the file to be renamed
485     * @param  to          the new target file
486     * @return             <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt>
487     * @throws IOException If an I/O error occurs during copy or delete operations.
488     */
489    public static boolean renameFileUsingCopy(Path from, Path to) throws IOException {
490        // do not try to rename non existing files
491        if (!Files.exists(from)) {
492            return false;
493        }
494
495        LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to);
496
497        copyFile(from, to);
498        if (!deleteFile(from)) {
499            throw new IOException(
500                    "Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from
501                                  + "' after copy succeeded");
502        }
503
504        return true;
505    }
506
507    /**
508     * Copies the file
509     *
510     * @param  from        the source file
511     * @param  to          the destination file
512     * @throws IOException If an I/O error occurs during copy operation
513     */
514    public static void copyFile(File from, File to) throws IOException {
515        copyFile(from.toPath(), to.toPath());
516    }
517
518    /**
519     * Copies the file
520     *
521     * @param  from        the source file
522     * @param  to          the destination file
523     * @throws IOException If an I/O error occurs during copy operation
524     */
525    public static void copyFile(Path from, Path to) throws IOException {
526        Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
527    }
528
529    /**
530     * Deletes the file.
531     * <p/>
532     * This implementation will attempt to delete the file up till three times with one second delay, which can mitigate
533     * problems on deleting files on some platforms such as Windows.
534     *
535     * @param file the file to delete
536     */
537    public static boolean deleteFile(File file) {
538        return deleteFile(file.toPath());
539    }
540
541    /**
542     * Deletes the file.
543     * <p/>
544     * This implementation will attempt to delete the file up till three times with one second delay, which can mitigate
545     * problems on deleting files on some platforms such as Windows.
546     *
547     * @param file the file to delete
548     */
549    public static boolean deleteFile(Path file) {
550        // do not try to delete non existing files
551        if (!Files.exists(file)) {
552            return false;
553        }
554
555        // some OS such as Windows can have problem doing delete IO operations so we may need to
556        // retry a couple of times to let it work
557        boolean deleted = false;
558        int count = 0;
559        while (!deleted && count < 3) {
560            LOG.debug("Retrying attempt {} to delete file: {}", count, file);
561
562            try {
563                Files.delete(file);
564                deleted = true;
565            } catch (IOException e) {
566                if (count > 0) {
567                    try {
568                        Thread.sleep(1000);
569                    } catch (InterruptedException ie) {
570                        LOG.info("Interrupted while trying to delete file {}", file, e);
571                        Thread.currentThread().interrupt();
572                    }
573                }
574            }
575            count++;
576        }
577
578        if (LOG.isDebugEnabled() && count > 0) {
579            LOG.debug("Tried {} to delete file: {} with result: {}", count, file, deleted);
580        }
581        return deleted;
582    }
583
584    /**
585     * Is the given file an absolute file.
586     * <p/>
587     * Will also work around issue on Windows to consider files on Windows starting with a \ as absolute files. This
588     * makes the logic consistent across all OS platforms.
589     *
590     * @param  file the file
591     * @return      <tt>true</ff> if it's an absolute path, <tt>false</tt> otherwise.
592     */
593    public static boolean isAbsolute(File file) {
594        if (isWindows()) {
595            // special for windows
596            String path = file.getPath();
597            if (path.startsWith(File.separator)) {
598                return true;
599            }
600        }
601        return file.isAbsolute();
602    }
603
604    /**
605     * Creates a new file.
606     *
607     * @param  file        the file
608     * @return             <tt>true</tt> if created a new file, <tt>false</tt> otherwise
609     * @throws IOException is thrown if error creating the new file
610     */
611    public static boolean createNewFile(File file) throws IOException {
612        // need to check first
613        if (file.exists()) {
614            return false;
615        }
616        try {
617            return file.createNewFile();
618        } catch (IOException e) {
619            // and check again if the file was created as createNewFile may create the file
620            // but throw a permission error afterwards when using some NAS
621            if (file.exists()) {
622                return true;
623            } else {
624                throw e;
625            }
626        }
627    }
628
629    /**
630     * Determines whether the URI has a scheme (e.g. file:, classpath: or http:)
631     *
632     * @param  uri the URI
633     * @return     <tt>true</tt> if the URI starts with a scheme
634     */
635    private static boolean hasScheme(String uri) {
636        if (uri == null) {
637            return false;
638        }
639
640        return uri.startsWith("file:") || uri.startsWith("classpath:") || uri.startsWith("http:");
641    }
642
643}