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