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