All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tangosol.io.FileHelper Maven / Gradle / Ivy

There is a newer version: 24.09
Show newest version
/*
 * Copyright (c) 2000, 2020, Oracle and/or its affiliates.
 *
 * Licensed under the Universal Permissive License v 1.0 as shown at
 * http://oss.oracle.com/licenses/upl.
 */
package com.tangosol.io;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import java.nio.channels.FileLock;

import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.CopyOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;

/**
 * A Collection of helper methods for files.
 *
 * @author jh  2012.07.13
 */
public class FileHelper
    {
    // ----- filename utility methods ---------------------------------------

    /**
     * Determine if the given character is unsafe to use in a filename.
     *
     * @param ch  the character in question
     *
     * @return true if the given character should be avoided in a filename
     */
    private static boolean isUnsafeChar(char ch)
        {
        // see:
        // http://www.mtu.edu/umc/services/web/resources/cms/characters-to-avoid.html
        // http://stackoverflow.com/questions/620605/how-to-make-a-valid-windows-filename-from-an-arbitrary-string/
        // http://www.unicodemap.org/range/1/Basic_Latin/

        return (ch >= 0x0000 && ch <= 0x0020) || // non-printable and whitespace
               (ch >= 0x0021 && ch <= 0x0027) || // !, ", #, $, %, &, '
                ch == '*'                     ||
                ch == '+'                     ||
                ch == '.'                     ||
                ch == '/'                     ||
                ch == ':'                     ||
               (ch >= 0x003c && ch <= 0x0040) || // <, =, >, ?, @
                ch == '\\'                    ||
                ch == '`'                     ||
               (ch >= 0x007b && ch <= 0x007d);   // {, |, }
        }

    /**
     * Given a string, return a derivative version that is safe to use for a
     * filename.
     *
     * @param sName  the string
     *
     * @return a derivative of the given string that is safe to use as a
     *         filename (may be the same as the string supplied)
     */
    public static String toFilename(String sName)
        {
        final char REPLACEMENT = '-';

        // if the given name is empty, return a single character string
        int cch = sName == null ? 0 : sName.length();
        if (cch == 0)
            {
            return String.valueOf(REPLACEMENT);
            }

        // scan the given string for unsafe characters, replacing them with
        // a safe alternative
        boolean fNew = false;
        char[]  ach  = new char[cch];
        for (int i = 0; i < cch; ++i)
            {
            char ch = sName.charAt(i);
            if (isUnsafeChar(ch))
                {
                ch   = REPLACEMENT;
                fNew = true;
                }
            ach[i] = ch;
            }

        return fNew ? new String(ach) : sName;
        }

    /**
     * Return the path of a file.
     * 

* The implementation attempts to get the canonical path and iff this call * abruptly fails is the absolute path returned. * * @param file the {@link File} to be interrogated to return the path * * @return the path of the provided file */ public static String getPath(File file) { try { return file.getCanonicalPath(); } catch (IOException ignore) {} return file.getAbsolutePath(); } // ----- directory utility methods -------------------------------------- /** * Validate that the given File represents a directory, creating it if * it doesn't already exist. * * @param file the File to check * * @return the validated File * * @throws IOException on error creating the directory */ public static File ensureDir(File file) throws IOException { if (file == null) { throw new IllegalArgumentException("null file"); } return file.isDirectory() ? file : Files.createDirectories(file.toPath()).toFile(); } /** * Validate that the given File exists and represents a directory. * * @param file the File to check * * @throws IOException if the given File doesn't exist or doesn't * represent a directory */ public static void validateDir(File file) throws IOException { if (file == null) { throw new IllegalArgumentException("null file"); } Path path = file.toPath(); if (!Files.isDirectory(path)) { throw new IOException("the specified path \"" + path + "\" is not a directory"); } } /** * Create a deep copy of the specified directory into the specified target * directory. * * @param fileDirFrom the directory to copy from * @param fileDirTo the directory to copy into * * @throws IOException if an error occurred copying the directory */ public static void copyDir(File fileDirFrom, File fileDirTo) throws IOException { validateDir(fileDirFrom); if (fileDirTo == null) { throw new IllegalArgumentException("null file"); } // ensure that the parent directory of the destination path exists File fileParent = fileDirTo.getParentFile(); if (fileParent != null) { ensureDir(fileParent); } Path pathFrom = fileDirFrom.toPath(); Path pathTo = fileDirTo.toPath(); Files.walkFileTree(pathFrom, new CopyDirVisitor(pathFrom, pathTo)); } /** * Delete a directory recursively. * * @param fileDir directory to delete * * @throws IOException if an error occurred deleting the directory */ public static void deleteDir(File fileDir) throws IOException { Path pathDir = fileDir.toPath(); if (Files.exists(pathDir)) { validateDir(fileDir); Files.walkFileTree(pathDir, new DeleteDirVisitor()); } } /** * Recursively delete a directory suppressing any raised exceptions. * * @param fileDir directory to delete */ public static void deleteDirSilent(File fileDir) { try { deleteDir(fileDir); } catch (Exception ioe) {} } /** * Move the specified directory to the specified destination path. * * @param fileDirFrom the directory to move * @param fileDirTo the destination path * * @throws IOException if an error occurred moving the directory */ public static void moveDir(File fileDirFrom, File fileDirTo) throws IOException { validateDir(fileDirFrom); if (fileDirTo == null) { throw new IllegalArgumentException("null file"); } // ensure that the parent directory of the destination path exists File fileParent = fileDirTo.getParentFile(); if (fileParent != null) { ensureDir(fileParent); } Path pathFrom = fileDirFrom.toPath(); Path pathTo = fileDirTo.toPath(); // first attempt an atomic move try { Files.move(pathFrom, pathTo, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } catch (AtomicMoveNotSupportedException e) { // if the atomic move failed, copy the directory to the // destination and then delete it Files.walkFileTree(pathFrom, new CopyDirVisitor(pathFrom, pathTo)); Files.walkFileTree(pathFrom, new DeleteDirVisitor()); } } /** * Create a unique temporary directory. * * @return a unique temporary directory */ public static File createTempDir() throws IOException { File file = Files.createTempDirectory(null).toFile(); file.deleteOnExit(); return file; } /** * Return the approximate disk usage in bytes for a given directory. Note * the size may differ from the actual size on the file system due to * compression, support for sparse files, or other reasons. * * @param fileDir the directory to size * * @return the number of total bytes used by the files in the directory */ public static long sizeDir(File fileDir) { Path pathDir = fileDir == null ? null : fileDir.toPath(); if (pathDir == null || !Files.isDirectory(pathDir)) { // COH-10194 // Ignore this as there is the possibility that one of the // directories as this operation is running. We don't want to // throw the exception, just return 0L to indicate we are not // counting size in this instance. return 0L; } SizeDirVisitor visitor = new SizeDirVisitor(); try { Files.walkFileTree(pathDir, visitor); } catch (IOException e) { // again, ignore as this is just an approximation } return visitor.getSize(); } // ----- file utility methods ------------------------------------------- /** * Copy the specified file according to the source and destination File * objects preserving the time-stamp of the source. * * @param fileFrom the {@link File} to copy from * @param fileTo the {@link File} to copy to * * @throws IOException if an error occurred copying the file */ public static void copyFile(File fileFrom, File fileTo) throws IOException { if (fileFrom == null || fileTo == null) { throw new IllegalArgumentException("null file"); } // ensure that the parent directory of the destination path exists File fileParent = fileTo.getParentFile(); if (fileParent != null) { ensureDir(fileParent); } Files.copy(fileFrom.toPath(), fileTo.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES, LinkOption.NOFOLLOW_LINKS); } /** * Move the specified file to the specified destination path. * * @param fileFrom the file to move * @param fileTo the destination path * * @throws IOException if an error occurred moving the file */ public static void moveFile(File fileFrom, File fileTo) throws IOException { if (fileFrom == null || fileTo == null) { throw new IllegalArgumentException("null file"); } // ensure that the parent directory of the destination path exists File fileParent = fileTo.getParentFile(); if (fileParent != null) { ensureDir(fileParent); } Files.move(fileFrom.toPath(), fileTo.toPath()); } /** * Obtain an exclusive lock on the specified file. The lock should be * released with the {@link #unlockFile(FileLock) unlockFile} method. * * @param file the file to lock * * @return a FileLock if an exclusive lock was obtained, * null otherwise */ public static FileLock lockFile(File file) { FileLock lock = null; FileOutputStream out = null; try { // try to obtain a lock on the lock file out = new FileOutputStream(file); lock = out.getChannel().tryLock(); } catch (Throwable t) { // fall through } finally { if (lock == null && out != null) { try { // will also close all Channels out.close(); } catch (IOException e) { // ignore } } } return lock; } /** * Release an {@link #lockFile(File) exclusive lock} on a file. * * @param lock the {@link #lockFile(File) exclusive lock} to release */ public static void unlockFile(FileLock lock) { // release the file lock try { lock.release(); } catch (IOException e) { // ignore } // close the lock file try { lock.channel().close(); } catch (IOException e) { // ignore } } /** * Checks if the character is a separator. * * @param ch the character to check * * @return true if it is a separator character */ private static boolean isSeparator(final char ch) { return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR; } /** * Converts all separators to the Unix separator of forward slash. * * @param path the path to be changed, null ignored * * @return the updated path */ public static String separatorsToUnix(final String path) { if (path == null || path.indexOf(WINDOWS_SEPARATOR) == NOT_FOUND) { return path; } return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR); } /** * Returns the length of the filename prefix, such as C:/ or * ~/. *

* This method will handle a file in either Unix or Windows format. *

* The prefix length includes the first slash in the full filename if * applicable. Thus, it is possible that the length returned is greater than * the length of the input string. *

     * Windows:
     * a\b\c.txt           --> ""          --> relative
     * \a\b\c.txt          --> "\"         --> current drive absolute
     * C:a\b\c.txt         --> "C:"        --> drive relative
     * C:\a\b\c.txt        --> "C:\"       --> absolute
     * \\server\a\b\c.txt  --> "\\server\" --> UNC
     * \\\a\b\c.txt        -->  error, length = -1
     *
     * Unix:
     * a/b/c.txt           --> ""          --> relative
     * /a/b/c.txt          --> "/"         --> absolute
     * ~/a/b/c.txt         --> "~/"        --> current user
     * ~                   --> "~/"        --> current user (slash added)
     * ~user/a/b/c.txt     --> "~user/"    --> named user
     * ~user               --> "~user/"    --> named user (slash added)
     * //server/a/b/c.txt  --> "//server/"
     * ///a/b/c.txt        --> error, length = -1
     * 
*

* The output will be the same irrespective of the machine that the code is * running on. ie. both Unix and Windows prefixes are matched regardless. *

* Note that a leading // (or \\) is used to indicate a UNC name on Windows. * These must be followed by a server name, so double-slashes are not * collapsed to a single slash at the start of the filename. * * @param filename the filename to find the prefix in, null returns -1 * * @return the length of the prefix, -1 if invalid or null */ public static int getPrefixLength(final String filename) { if (filename == null) { return NOT_FOUND; } final int len = filename.length(); if (len == 0) { return 0; } char ch0 = filename.charAt(0); if (ch0 == ':') { return NOT_FOUND; } if (len == 1) { if (ch0 == '~') { return 2; // return a length greater than the input } return isSeparator(ch0) ? 1 : 0; } else { if (ch0 == '~') { int posUnix = filename.indexOf(UNIX_SEPARATOR, 1); int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1); if (posUnix == NOT_FOUND && posWin == NOT_FOUND) { return len + 1; // return a length greater than the input } posUnix = posUnix == NOT_FOUND ? posWin : posUnix; posWin = posWin == NOT_FOUND ? posUnix : posWin; return Math.min(posUnix, posWin) + 1; } final char ch1 = filename.charAt(1); if (ch1 == ':') { ch0 = Character.toUpperCase(ch0); if (ch0 >= 'A' && ch0 <= 'Z') { if (len == 2 || isSeparator(filename.charAt(2)) == false) { return 2; } return 3; } return NOT_FOUND; } else if (isSeparator(ch0) && isSeparator(ch1)) { int posUnix = filename.indexOf(UNIX_SEPARATOR, 2); int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2); if (posUnix == NOT_FOUND && posWin == NOT_FOUND || posUnix == 2 || posWin == 2) { return NOT_FOUND; } posUnix = posUnix == NOT_FOUND ? posWin : posUnix; posWin = posWin == NOT_FOUND ? posUnix : posWin; return Math.min(posUnix, posWin) + 1; } else { return isSeparator(ch0) ? 1 : 0; } } } /** * Returns the index of the last directory separator character. *

* This method will handle a file in either Unix or Windows format. The * position of the last forward or backslash is returned. *

* The output will be the same irrespective of the machine that the code is * running on. * * @param filename the filename to find the last path separator in, null * returns -1 * * @return the index of the last separator character, or -1 if there is no * such character */ public static int indexOfLastSeparator(final String filename) { if (filename == null) { return NOT_FOUND; } final int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR); final int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR); return Math.max(lastUnixPos, lastWindowsPos); } /** * Returns the index of the last extension separator character, which is a * dot.

This method also checks that there is no directory separator * after the last dot. To do this it uses {@link #indexOfLastSeparator(String)} * which will handle a file in either Unix or Windows format.

The * output will be the same irrespective of the machine that the code is * running on.

* * @param filename the filename to find the last extension separator in, * null returns -1 * * @return the index of the last extension separator character, or -1 if * there is no such character */ public static int indexOfExtension(final String filename) { if (filename == null) { return NOT_FOUND; } final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR); final int lastSeparator = indexOfLastSeparator(filename); return lastSeparator > extensionPos ? NOT_FOUND : extensionPos; } /** * Gets the path from a full filename, which excludes the prefix, and also * excluding the final directory separator. *

* This method will handle a file in either Unix or Windows format. The * method is entirely text based, and returns the text before the last * forward or backslash. *

     * C:\a\b\c.txt --> a\b
     * ~/a/b/c.txt  --> a/b
     * a.txt        --> ""
     * a/b/c        --> a/b
     * a/b/c/       --> a/b/c
     * 
*

* The output will be the same irrespective of the machine that the code is * running on. * * @param filename the filename to query, null returns null * * @return the path of the file, an empty string if none exists, null if * invalid. Null bytes inside string will be removed */ public static String getPathNoEndSeparator(final String filename) { return doGetPath(filename, 0); } /** * Does the work of getting the path. * * @param filename the filename * @param separatorAdd 0 to omit the end separator, 1 to return it * * @return the path. Null bytes inside string will be removed */ static String doGetPath(final String filename, final int separatorAdd) { if (filename == null) { return null; } final int prefix = getPrefixLength(filename); if (prefix < 0) { return null; } final int index = indexOfLastSeparator(filename); final int endIndex = index + separatorAdd; if (prefix >= filename.length() || index < 0 || prefix >= endIndex) { return ""; } return filterNullBytes(filename.substring(prefix, endIndex)); } /** * Gets the name minus the path from a full filename. *

* This method will handle a file in either Unix or Windows format. The text * after the last forward or backslash is returned. *

     * a/b/c.txt --> c.txt
     * a.txt     --> a.txt
     * a/b/c     --> c
     * a/b/c/    --> ""
     * 
*

* The output will be the same irrespective of the machine that the code is * running on. * * @param filename the filename to query, null returns null * * @return the name of the file without the path, or an empty string if none * exists. Null bytes inside string will be removed */ public static String getName(final String filename) { if (filename == null) { return null; } String cleanFileName = filterNullBytes(filename); final int index = indexOfLastSeparator(cleanFileName); return cleanFileName.substring(index + 1); } /** * Filters the supplied path for null byte characters. Can be used for * normalizations to avoid poison byte attacks. *

* This mimicks behaviour of 1.7u40+. Once minimum java requirement is above * this version, this code can be removed. * * @param path the path * * @return the supplied string without any embedded null characters */ static String filterNullBytes(String path) { return path.contains("\u0000") ? path.replace("\u0000", "") : path; } /** * Gets the base name, minus the full path and extension, from a full * filename. *

* This method will handle a file in either Unix or Windows format. The text * after the last forward or backslash and before the last dot is returned. *

     * a/b/c.txt --> c
     * a.txt     --> a
     * a/b/c     --> c
     * a/b/c/    --> ""
     * 
*

* The output will be the same irrespective of the machine that the code is * running on. * * @param filename the filename to query, null returns null * * @return the name of the file without the path, or an empty string if none * exists. Null bytes inside string will be removed */ public static String getBaseName(final String filename) { return removeExtension(getName(filename)); } /** * Gets the extension of a filename. *

* This method returns the textual part of the filename after the last dot. * There must be no directory separator after the dot. *

     * foo.txt      --> "txt"
     * a/b/c.jpg    --> "jpg"
     * a/b.txt/c    --> ""
     * a/b/c        --> ""
     * 
*

* The output will be the same irrespective of the machine that the code is * running on. * * @param filename the filename to retrieve the extension of. * * @return the extension of the file or an empty string if none exists or * {@code null} if the filename is {@code null}. */ public static String getExtension(final String filename) { if (filename == null) { return null; } final int index = indexOfExtension(filename); if (index == NOT_FOUND) { return ""; } else { return filename.substring(index + 1); } } /** * Removes the extension from a filename. *

* This method returns the textual part of the filename before the last dot. * There must be no directory separator after the dot. *

     * foo.txt    --> foo
     * a\b\c.jpg  --> a\b\c
     * a\b\c      --> a\b\c
     * a.b\c      --> a.b\c
     * 
*

* The output will be the same irrespective of the machine that the code is * running on. * * @param filename the filename to query, null returns null * * @return the filename minus the extension */ public static String removeExtension(final String filename) { if (filename == null) { return null; } String cleanFileName = filterNullBytes(filename); final int index = indexOfExtension(cleanFileName); if (index == NOT_FOUND) { return cleanFileName; } else { return cleanFileName.substring(0, index); } } // ----- static helper classes ------------------------------------------ /** * Extension of SimpleFileVisitor used to copy a directory and * it's contents. */ private static class CopyDirVisitor extends SimpleFileVisitor { // ----- constructors ----------------------------------------------- /** * Create a new CopyDirVisitor that will copy the given source * directory to the specified destination. * * @param pathFrom the source directory * @param pathTo the destination directory */ public CopyDirVisitor(Path pathFrom, Path pathTo) { this(pathFrom, pathTo, StandardCopyOption.REPLACE_EXISTING, LinkOption.NOFOLLOW_LINKS); } /** * Create a new CopyDirVisitor that will copy the given source * directory to the specified destination with the given options. * * @param pathFrom the source directory * @param pathTo the destination directory * @param opts the copy options */ public CopyDirVisitor(Path pathFrom, Path pathTo, CopyOption... opts) { f_pathFrom = pathFrom; f_pathTo = pathTo; f_opts = opts; } // ----- SimpleFileVisitor overrides -------------------------------- @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Path pathTo = f_pathTo.resolve(f_pathFrom.relativize(dir)); if (!Files.exists(pathTo)) { Files.createDirectory(pathTo); } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.copy(file, f_pathTo.resolve(f_pathFrom.relativize(file)), f_opts); return FileVisitResult.CONTINUE; } // ----- data members ----------------------------------------------- private final Path f_pathFrom; private final Path f_pathTo; private final CopyOption[] f_opts; } /** * Extension of SimpleFileVisitor used to delete a directory and * it's contents. */ private static class DeleteDirVisitor extends SimpleFileVisitor { // ----- SimpleFileVisitor overrides -------------------------------- @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { super.visitFile(file, attrs); Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { super.postVisitDirectory(dir, e); Files.delete(dir); return FileVisitResult.CONTINUE; } } /** * Extension of SimpleFileVisitor used to size a directory and * it's contents. */ private static class SizeDirVisitor extends SimpleFileVisitor { // ----- SimpleFileVisitor overrides -------------------------------- @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { m_cb += Files.size(file); return FileVisitResult.CONTINUE; } // ----- accessors -------------------------------------------------- /** * Return the calculated size of the directory and it's contents. * * @return the size (in bytes) of the directory and it's contents */ public long getSize() { return m_cb; } // ----- data members ----------------------------------------------- private long m_cb; } // ---- constants ------------------------------------------------------- /** * The extension separator character. */ private static final char EXTENSION_SEPARATOR = '.'; /** * A value returned when a character ot string is not found. */ private static final int NOT_FOUND = -1; /** * The Unix separator character. */ private static final char UNIX_SEPARATOR = '/'; /** * The Windows separator character. */ private static final char WINDOWS_SEPARATOR = '\\'; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy