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 boolean windowsOs = 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 windowsOs;
084    }
085
086    public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException {
087        Objects.requireNonNull(parentDir);
088
089        if (suffix == null) {
090            suffix = ".tmp";
091        }
092        if (prefix == null) {
093            prefix = "camel";
094        } else if (prefix.length() < 3) {
095            prefix = prefix + "camel";
096        }
097
098        // create parent folder
099        parentDir.mkdirs();
100
101        return Files.createTempFile(parentDir.toPath(), prefix, suffix).toFile();
102    }
103
104    /**
105     * Strip any leading separators
106     */
107    public static String stripLeadingSeparator(String name) {
108        if (name == null) {
109            return null;
110        }
111        while (name.startsWith("/") || name.startsWith(File.separator)) {
112            name = name.substring(1);
113        }
114        return name;
115    }
116
117    /**
118     * Does the name start with a leading separator
119     */
120    public static boolean hasLeadingSeparator(String name) {
121        if (name == null) {
122            return false;
123        }
124        if (name.startsWith("/") || name.startsWith(File.separator)) {
125            return true;
126        }
127        return false;
128    }
129
130    /**
131     * Strip first leading separator
132     */
133    public static String stripFirstLeadingSeparator(String name) {
134        if (name == null) {
135            return null;
136        }
137        if (name.startsWith("/") || name.startsWith(File.separator)) {
138            name = name.substring(1);
139        }
140        return name;
141    }
142
143    /**
144     * Strip any trailing separators
145     */
146    public static String stripTrailingSeparator(String name) {
147        if (ObjectHelper.isEmpty(name)) {
148            return name;
149        }
150
151        String s = name;
152
153        // there must be some leading text, as we should only remove trailing separators
154        while (s.endsWith("/") || s.endsWith(File.separator)) {
155            s = s.substring(0, s.length() - 1);
156        }
157
158        // if the string is empty, that means there was only trailing slashes, and no leading text
159        // and so we should then return the original name as is
160        if (ObjectHelper.isEmpty(s)) {
161            return name;
162        } else {
163            // return without trailing slashes
164            return s;
165        }
166    }
167
168    /**
169     * Strips any leading paths
170     */
171    public static String stripPath(String name) {
172        if (name == null) {
173            return null;
174        }
175        int posUnix = name.lastIndexOf('/');
176        int posWin = name.lastIndexOf('\\');
177        int pos = Math.max(posUnix, posWin);
178
179        if (pos != -1) {
180            return name.substring(pos + 1);
181        }
182        return name;
183    }
184
185    public static String stripExt(String name) {
186        return stripExt(name, false);
187    }
188
189    public static String stripExt(String name, boolean singleMode) {
190        if (name == null) {
191            return null;
192        }
193
194        // the name may have a leading path
195        int posUnix = name.lastIndexOf('/');
196        int posWin = name.lastIndexOf('\\');
197        int pos = Math.max(posUnix, posWin);
198
199        if (pos > 0) {
200            String onlyName = name.substring(pos + 1);
201            int pos2 = singleMode ? onlyName.lastIndexOf('.') : onlyName.indexOf('.');
202            if (pos2 > 0) {
203                return name.substring(0, pos + pos2 + 1);
204            }
205        } else {
206            // if single ext mode, then only return last extension
207            int pos2 = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
208            if (pos2 > 0) {
209                return name.substring(0, pos2);
210            }
211        }
212
213        return name;
214    }
215
216    public static String onlyExt(String name) {
217        return onlyExt(name, false);
218    }
219
220    public static String onlyExt(String name, boolean singleMode) {
221        if (name == null) {
222            return null;
223        }
224        name = stripPath(name);
225
226        // extension is the first dot, as a file may have double extension such as .tar.gz
227        // if single ext mode, then only return last extension
228        int pos = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
229        if (pos != -1) {
230            return name.substring(pos + 1);
231        }
232        return null;
233    }
234
235    /**
236     * Returns only the leading path (returns <tt>null</tt> if no path)
237     */
238    public static String onlyPath(String name) {
239        if (name == null) {
240            return null;
241        }
242
243        int posUnix = name.lastIndexOf('/');
244        int posWin = name.lastIndexOf('\\');
245        int pos = Math.max(posUnix, posWin);
246
247        if (pos > 0) {
248            return name.substring(0, pos);
249        } else if (pos == 0) {
250            // name is in the root path, so extract the path as the first char
251            return name.substring(0, 1);
252        }
253        // no path in name
254        return null;
255    }
256
257    public static String onlyName(String name) {
258        return onlyName(name, false);
259    }
260
261    public static String onlyName(String name, boolean singleMode) {
262        name = FileUtil.stripPath(name);
263        name = FileUtil.stripExt(name, singleMode);
264
265        return name;
266    }
267
268    /**
269     * Compacts a path by stacking it and reducing <tt>..</tt>, and uses OS specific file separators (eg
270     * {@link java.io.File#separator}).
271     */
272    public static String compactPath(String path) {
273        return compactPath(path, "" + File.separatorChar);
274    }
275
276    /**
277     * Compacts a path by stacking it and reducing <tt>..</tt>, and uses the given separator.
278     *
279     */
280    public static String compactPath(String path, char separator) {
281        return compactPath(path, "" + 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                // do nothing because we don't want a path like foo/./bar or foo//bar
336            } else {
337                stack.push(part);
338            }
339        }
340
341        // build path based on stack
342        StringBuilder sb = new StringBuilder();
343        if (scheme != null) {
344            sb.append(scheme);
345            sb.append(":");
346        }
347
348        for (int i = 0; i < cntSlashsAtStart; i++) {
349            sb.append(separator);
350        }
351
352        // now we build back using FIFO so need to use descending
353        for (Iterator<String> it = stack.descendingIterator(); it.hasNext();) {
354            sb.append(it.next());
355            if (it.hasNext()) {
356                sb.append(separator);
357            }
358        }
359
360        if (endsWithSlash && !stack.isEmpty()) {
361            sb.append(separator);
362        }
363
364        return sb.toString();
365    }
366
367    public static void removeDir(File d) {
368        String[] list = d.list();
369        if (list == null) {
370            list = new String[0];
371        }
372        for (String s : list) {
373            File f = new File(d, s);
374            if (f.isDirectory()) {
375                removeDir(f);
376            } else {
377                delete(f);
378            }
379        }
380        delete(d);
381    }
382
383    private static void delete(File f) {
384        if (!f.delete()) {
385            try {
386                Thread.sleep(RETRY_SLEEP_MILLIS);
387            } catch (InterruptedException ex) {
388                // Ignore Exception
389            }
390            if (!f.delete()) {
391                f.deleteOnExit();
392            }
393        }
394    }
395
396    /**
397     * Renames a file.
398     *
399     * @param  from                      the from file
400     * @param  to                        the to file
401     * @param  copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
402     * @return                           <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
403     * @throws java.io.IOException       is thrown if error renaming file
404     */
405    public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
406        // do not try to rename non existing files
407        if (!from.exists()) {
408            return false;
409        }
410
411        // some OS such as Windows can have problem doing rename IO operations so we may need to
412        // retry a couple of times to let it work
413        boolean renamed = false;
414        int count = 0;
415        while (!renamed && count < 3) {
416            if (LOG.isDebugEnabled() && count > 0) {
417                LOG.debug("Retrying attempt {} to rename file from: {} to: {}", count, from, to);
418            }
419
420            renamed = from.renameTo(to);
421            if (!renamed && count > 0) {
422                try {
423                    Thread.sleep(1000);
424                } catch (InterruptedException e) {
425                    // ignore
426                }
427            }
428            count++;
429        }
430
431        // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
432        // for example if you move files between different file systems (linux -> windows etc.)
433        if (!renamed && copyAndDeleteOnRenameFail) {
434            // now do a copy and delete as all rename attempts failed
435            LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
436            renamed = renameFileUsingCopy(from, to);
437        }
438
439        if (LOG.isDebugEnabled() && count > 0) {
440            LOG.debug("Tried {} to rename file: {} to: {} with result: {}", count, from, to, renamed);
441        }
442        return renamed;
443    }
444
445    /**
446     * Rename file using copy and delete strategy. This is primarily used in environments where the regular rename
447     * operation is unreliable.
448     *
449     * @param  from        the file to be renamed
450     * @param  to          the new target file
451     * @return             <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt>
452     * @throws IOException If an I/O error occurs during copy or delete operations.
453     */
454    public static boolean renameFileUsingCopy(File from, File to) throws IOException {
455        // do not try to rename non existing files
456        if (!from.exists()) {
457            return false;
458        }
459
460        LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to);
461
462        copyFile(from, to);
463        if (!deleteFile(from)) {
464            throw new IOException(
465                    "Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from
466                                  + "' after copy succeeded");
467        }
468
469        return true;
470    }
471
472    /**
473     * Copies the file
474     *
475     * @param  from        the source file
476     * @param  to          the destination file
477     * @throws IOException If an I/O error occurs during copy operation
478     */
479    public static void copyFile(File from, File to) throws IOException {
480        Files.copy(from.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING);
481    }
482
483    /**
484     * Deletes the file.
485     * <p/>
486     * This implementation will attempt to delete the file up till three times with one second delay, which can mitigate
487     * problems on deleting files on some platforms such as Windows.
488     *
489     * @param file the file to delete
490     */
491    public static boolean deleteFile(File file) {
492        // do not try to delete non existing files
493        if (!file.exists()) {
494            return false;
495        }
496
497        // some OS such as Windows can have problem doing delete IO operations so we may need to
498        // retry a couple of times to let it work
499        boolean deleted = false;
500        int count = 0;
501        while (!deleted && count < 3) {
502            LOG.debug("Retrying attempt {} to delete file: {}", count, file);
503
504            deleted = file.delete();
505            if (!deleted && count > 0) {
506                try {
507                    Thread.sleep(1000);
508                } catch (InterruptedException e) {
509                    // ignore
510                }
511            }
512            count++;
513        }
514
515        if (LOG.isDebugEnabled() && count > 0) {
516            LOG.debug("Tried {} to delete file: {} with result: {}", count, file, deleted);
517        }
518        return deleted;
519    }
520
521    /**
522     * Is the given file an absolute file.
523     * <p/>
524     * Will also work around issue on Windows to consider files on Windows starting with a \ as absolute files. This
525     * makes the logic consistent across all OS platforms.
526     *
527     * @param  file the file
528     * @return      <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise.
529     */
530    public static boolean isAbsolute(File file) {
531        if (isWindows()) {
532            // special for windows
533            String path = file.getPath();
534            if (path.startsWith(File.separator)) {
535                return true;
536            }
537        }
538        return file.isAbsolute();
539    }
540
541    /**
542     * Creates a new file.
543     *
544     * @param  file        the file
545     * @return             <tt>true</tt> if created a new file, <tt>false</tt> otherwise
546     * @throws IOException is thrown if error creating the new file
547     */
548    public static boolean createNewFile(File file) throws IOException {
549        // need to check first
550        if (file.exists()) {
551            return false;
552        }
553        try {
554            return file.createNewFile();
555        } catch (IOException e) {
556            // and check again if the file was created as createNewFile may create the file
557            // but throw a permission error afterwards when using some NAS
558            if (file.exists()) {
559                return true;
560            } else {
561                throw e;
562            }
563        }
564    }
565
566    /**
567     * Determines whether the URI has a scheme (e.g. file:, classpath: or http:)
568     *
569     * @param  uri the URI
570     * @return     <tt>true</tt> if the URI starts with a scheme
571     */
572    private static boolean hasScheme(String uri) {
573        if (uri == null) {
574            return false;
575        }
576
577        return uri.startsWith("file:") || uri.startsWith("classpath:") || uri.startsWith("http:");
578    }
579
580}