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