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 Files.createTempFile(parent.toPath(), prefix, suffix).toFile();
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 file 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        if (path.startsWith("http:")) {
292            return path;
293        }
294        
295        // only normalize if contains a path separator
296        if (path.indexOf('/') == -1 && path.indexOf('\\') == -1)  {
297            return path;
298        }
299
300        // need to normalize path before compacting
301        path = normalizePath(path);
302
303        // preserve ending slash if given in input path
304        boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\");
305
306        // preserve starting slash if given in input path
307        int cntSlashsAtStart = 0;
308        if (path.startsWith("/") || path.startsWith("\\")) {
309            cntSlashsAtStart++;
310            // for Windows, preserve up to 2 starting slashes, which is necessary for UNC paths.
311            if (isWindows() && path.length() > 1 && (path.charAt(1) == '/' || path.charAt(1) == '\\')) {
312                cntSlashsAtStart++;
313            }
314        }
315        
316        Deque<String> stack = new ArrayDeque<>();
317
318        // separator can either be windows or unix style
319        String separatorRegex = "\\\\|/";
320        String[] parts = path.split(separatorRegex);
321        for (String part : parts) {
322            if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) {
323                // only pop if there is a previous path, which is not a ".." path either
324                stack.pop();
325            } else if (part.equals(".") || part.isEmpty()) {
326                // do nothing because we don't want a path like foo/./bar or foo//bar
327            } else {
328                stack.push(part);
329            }
330        }
331
332        // build path based on stack
333        StringBuilder sb = new StringBuilder();
334        
335        for (int i = 0; i < cntSlashsAtStart; i++) {
336            sb.append(separator);
337        }
338
339        // now we build back using FIFO so need to use descending
340        for (Iterator<String> it = stack.descendingIterator(); it.hasNext();) {
341            sb.append(it.next());
342            if (it.hasNext()) {
343                sb.append(separator);
344            }
345        }
346
347        if (endsWithSlash && stack.size() > 0) {
348            sb.append(separator);
349        }
350
351        return sb.toString();
352    }
353
354    @Deprecated
355    private static synchronized File getDefaultTempDir() {
356        if (defaultTempDir != null && defaultTempDir.exists()) {
357            return defaultTempDir;
358        }
359
360        defaultTempDir = createNewTempDir();
361
362        // create shutdown hook to remove the temp dir
363        shutdownHook = new Thread() {
364            @Override
365            public void run() {
366                removeDir(defaultTempDir);
367            }
368        };
369        Runtime.getRuntime().addShutdownHook(shutdownHook);
370
371        return defaultTempDir;
372    }
373
374    /**
375     * Creates a new temporary directory in the <tt>java.io.tmpdir</tt> directory.
376     */
377    @Deprecated
378    private static File createNewTempDir() {
379        String s = System.getProperty("java.io.tmpdir");
380        File checkExists = new File(s);
381        if (!checkExists.exists()) {
382            throw new RuntimeException("The directory "
383                                   + checkExists.getAbsolutePath()
384                                   + " does not exist, please set java.io.tempdir"
385                                   + " to an existing directory");
386        }
387        
388        if (!checkExists.canWrite()) {
389            throw new RuntimeException("The directory "
390                + checkExists.getAbsolutePath()
391                + " is not writable, please set java.io.tempdir"
392                + " to a writable directory");
393        }
394
395        // create a sub folder with a random number
396        Random ran = new Random();
397        int x = ran.nextInt(1000000);
398        File f = new File(s, "camel-tmp-" + x);
399        int count = 0;
400        // Let us just try 100 times to avoid the infinite loop
401        while (!f.mkdir()) {
402            count++;
403            if (count >= 100) {
404                throw new RuntimeException("Camel cannot a temp directory from"
405                    + checkExists.getAbsolutePath()
406                    + " 100 times , please set java.io.tempdir"
407                    + " to a writable directory");
408            }
409            x = ran.nextInt(1000000);
410            f = new File(s, "camel-tmp-" + x);
411        }
412
413        return f;
414    }
415
416    /**
417     * Shutdown and cleanup the temporary directory and removes any shutdown hooks in use.
418     */
419    @Deprecated
420    public static synchronized void shutdown() {
421        if (defaultTempDir != null && defaultTempDir.exists()) {
422            removeDir(defaultTempDir);
423        }
424
425        if (shutdownHook != null) {
426            Runtime.getRuntime().removeShutdownHook(shutdownHook);
427            shutdownHook = null;
428        }
429    }
430
431    public static void removeDir(File d) {
432        String[] list = d.list();
433        if (list == null) {
434            list = new String[0];
435        }
436        for (String s : list) {
437            File f = new File(d, s);
438            if (f.isDirectory()) {
439                removeDir(f);
440            } else {
441                delete(f);
442            }
443        }
444        delete(d);
445    }
446
447    private static void delete(File f) {
448        if (!f.delete()) {
449            if (isWindows()) {
450                System.gc();
451            }
452            try {
453                Thread.sleep(RETRY_SLEEP_MILLIS);
454            } catch (InterruptedException ex) {
455                // Ignore Exception
456            }
457            if (!f.delete()) {
458                f.deleteOnExit();
459            }
460        }
461    }
462
463    /**
464     * Renames a file.
465     *
466     * @param from the from file
467     * @param to   the to file
468     * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
469     * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
470     * @throws java.io.IOException is thrown if error renaming file
471     */
472    public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
473        // do not try to rename non existing files
474        if (!from.exists()) {
475            return false;
476        }
477
478        // some OS such as Windows can have problem doing rename IO operations so we may need to
479        // retry a couple of times to let it work
480        boolean renamed = false;
481        int count = 0;
482        while (!renamed && count < 3) {
483            if (LOG.isDebugEnabled() && count > 0) {
484                LOG.debug("Retrying attempt {} to rename file from: {} to: {}", count, from, to);
485            }
486
487            renamed = from.renameTo(to);
488            if (!renamed && count > 0) {
489                try {
490                    Thread.sleep(1000);
491                } catch (InterruptedException e) {
492                    // ignore
493                }
494            }
495            count++;
496        }
497
498        // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
499        // for example if you move files between different file systems (linux -> windows etc.)
500        if (!renamed && copyAndDeleteOnRenameFail) {
501            // now do a copy and delete as all rename attempts failed
502            LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
503            renamed = renameFileUsingCopy(from, to);
504        }
505
506        if (LOG.isDebugEnabled() && count > 0) {
507            LOG.debug("Tried {} to rename file: {} to: {} with result: {}", count, from, to, renamed);
508        }
509        return renamed;
510    }
511
512    /**
513     * Rename file using copy and delete strategy. This is primarily used in
514     * environments where the regular rename operation is unreliable.
515     * 
516     * @param from the file to be renamed
517     * @param to the new target file
518     * @return <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt>
519     * @throws IOException If an I/O error occurs during copy or delete operations.
520     */
521    public static boolean renameFileUsingCopy(File from, File to) throws IOException {
522        // do not try to rename non existing files
523        if (!from.exists()) {
524            return false;
525        }
526
527        LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to);
528
529        copyFile(from, to);
530        if (!deleteFile(from)) {
531            throw new IOException("Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from + "' after copy succeeded");
532        }
533
534        return true;
535    }
536
537    /**
538     * Copies the file
539     *
540     * @param from  the source file
541     * @param to    the destination file
542     * @throws IOException If an I/O error occurs during copy operation
543     */
544    public static void copyFile(File from, File to) throws IOException {
545        Files.copy(from.toPath(), to.toPath(), StandardCopyOption.REPLACE_EXISTING);
546    }
547
548    /**
549     * Deletes the file.
550     * <p/>
551     * This implementation will attempt to delete the file up till three times with one second delay, which
552     * can mitigate problems on deleting files on some platforms such as Windows.
553     *
554     * @param file  the file to delete
555     */
556    public static boolean deleteFile(File file) {
557        // do not try to delete non existing files
558        if (!file.exists()) {
559            return false;
560        }
561
562        // some OS such as Windows can have problem doing delete IO operations so we may need to
563        // retry a couple of times to let it work
564        boolean deleted = false;
565        int count = 0;
566        while (!deleted && count < 3) {
567            LOG.debug("Retrying attempt {} to delete file: {}", count, file);
568
569            deleted = file.delete();
570            if (!deleted && count > 0) {
571                try {
572                    Thread.sleep(1000);
573                } catch (InterruptedException e) {
574                    // ignore
575                }
576            }
577            count++;
578        }
579
580
581        if (LOG.isDebugEnabled() && count > 0) {
582            LOG.debug("Tried {} to delete file: {} with result: {}", count, file, deleted);
583        }
584        return deleted;
585    }
586
587    /**
588     * Is the given file an absolute file.
589     * <p/>
590     * Will also work around issue on Windows to consider files on Windows starting with a \
591     * as absolute files. This makes the logic consistent across all OS platforms.
592     *
593     * @param file  the file
594     * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise.
595     */
596    public static boolean isAbsolute(File file) {
597        if (isWindows()) {
598            // special for windows
599            String path = file.getPath();
600            if (path.startsWith(File.separator)) {
601                return true;
602            }
603        }
604        return file.isAbsolute();
605    }
606
607    /**
608     * Creates a new file.
609     *
610     * @param file the file
611     * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise
612     * @throws IOException is thrown if error creating the new file
613     */
614    public static boolean createNewFile(File file) throws IOException {
615        // need to check first
616        if (file.exists()) {
617            return false;
618        }
619        try {
620            return file.createNewFile();
621        } catch (IOException e) {
622            // and check again if the file was created as createNewFile may create the file
623            // but throw a permission error afterwards when using some NAS
624            if (file.exists()) {
625                return true;
626            } else {
627                throw e;
628            }
629        }
630    }
631
632}