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