com.android.tradefed.util.FileUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tradefed.util.FileUtil.java

Source

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tradefed.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;

import com.android.ddmlib.Log;
import com.android.tradefed.command.FatalHostError;
import com.android.tradefed.log.LogUtil.CLog;

/**
 * A helper class for file related operations
 */
public class FileUtil {

    private static final String LOG_TAG = "FileUtil";
    /**
     * The minimum allowed disk space in megabytes. File creation methods will
     * throw {@link LowDiskSpaceException} if the usable disk space in desired
     * partition is less than this amount.
     */
    private static final long MIN_DISK_SPACE_MB = 100;
    /** The min disk space in bytes */
    private static final long MIN_DISK_SPACE = MIN_DISK_SPACE_MB * 1024 * 1024;

    private static final char[] SIZE_SPECIFIERS = { ' ', 'K', 'M', 'G', 'T' };

    /**
     * Thrown if usable disk space is below minimum threshold.
     */
    @SuppressWarnings("serial")
    public static class LowDiskSpaceException extends FatalHostError {

        LowDiskSpaceException(String msg, Throwable cause) {
            super(msg, cause);
        }

        LowDiskSpaceException(String msg) {
            super(msg);
        }

    }

    /**
     * Method to create a chain of directories, and set them all group
     * execute/read/writable as they are created, by calling
     * {@link #chmodGroupRWX(File)}. Essentially a version of
     * {@link File#mkdirs()} that also runs {@link #chmod(File, String)}.
     *
     * @param file
     *            the name of the directory to create, possibly with containing
     *            directories that don't yet exist.
     * @return {@code true} if {@code file} exists and is a directory,
     *         {@code false} otherwise.
     */
    public static boolean mkdirsRWX(File file) {
        File parent = file.getParentFile();

        if (parent != null && !parent.isDirectory()) {
            // parent doesn't exist. recurse upward, which should both mkdir and
            // chmod
            if (!mkdirsRWX(parent)) {
                // Couldn't mkdir parent, fail
                Log.w(LOG_TAG, String.format("Failed to mkdir parent dir %s.", parent));
                return false;
            }
        }

        // by this point the parent exists. Try to mkdir file
        if (file.isDirectory() || file.mkdir()) {
            // file should exist. Try chmod and complain if that fails, but keep
            // going
            boolean setPerms = chmodGroupRWX(file);
            if (!setPerms) {
                Log.w(LOG_TAG, String.format("Failed to set dir %s to be group accessible.", file));
            }
        }

        return file.isDirectory();
    }

    public static boolean chmodRWXRecursively(File file) {
        boolean success = true;
        if (!file.setExecutable(true, false)) {
            CLog.w("Failed to set %s executable.", file.getAbsolutePath());
            success = false;
        }
        if (!file.setWritable(true, false)) {
            CLog.w("Failed to set %s writable.", file.getAbsolutePath());
            success = false;
        }
        if (!file.setReadable(true, false)) {
            CLog.w("Failed to set %s readable", file.getAbsolutePath());
            success = false;
        }

        if (file.isDirectory()) {
            File[] childs = file.listFiles();
            for (File child : childs) {
                if (!chmodRWXRecursively(child)) {
                    success = false;
                }
            }

        }
        return success;
    }

    public static boolean chmod(File file, String perms) {
        Log.d(LOG_TAG, String.format("Attempting to chmod %s to %s", file.getAbsolutePath(), perms));
        CommandResult result = RunUtil.getDefault().runTimedCmd(10 * 1000, "chmod", perms, file.getAbsolutePath());
        return result.getStatus().equals(CommandStatus.SUCCESS);
    }

    /**
     * Performs a best effort attempt to make given file group readable and
     * writable.
     * <p />
     * Note that the execute permission is required to make directories
     * accessible. See {@link #chmodGroupRWX(File)}.
     * <p/ >
     * If 'chmod' system command is not supported by underlying OS, will set
     * file to writable by all.
     *
     * @param file
     *            the {@link File} to make owner and group writable
     * @return <code>true</code> if file was successfully made group writable,
     *         <code>false</code> otherwise
     */
    public static boolean chmodGroupRW(File file) {
        if (chmod(file, "ug+rw")) {
            return true;
        } else {
            Log.d(LOG_TAG, String.format("Failed chmod; attempting to set %s globally RW", file.getAbsolutePath()));
            return file.setWritable(true, false /* false == writable for all */)
                    && file.setReadable(true, false /* false == readable for all */);
        }
    }

    /**
     * Performs a best effort attempt to make given file group executable,
     * readable, and writable.
     * <p/ >
     * If 'chmod' system command is not supported by underlying OS, will attempt
     * to set permissions for all users.
     *
     * @param file
     *            the {@link File} to make owner and group writable
     * @return <code>true</code> if permissions were set successfully,
     *         <code>false</code> otherwise
     */
    public static boolean chmodGroupRWX(File file) {
        if (chmod(file, "ug+rwx")) {
            return true;
        } else {
            Log.d(LOG_TAG,
                    String.format("Failed chmod; attempting to set %s globally RWX", file.getAbsolutePath()));
            return file.setExecutable(true, false /* false == executable for all */)
                    && file.setWritable(true, false /* false == writable for all */)
                    && file.setReadable(true, false /* false == readable for all */);
        }
    }

    /**
     * Helper function to create a temp directory in the system default
     * temporary file directory.
     *
     * @param prefix
     *            The prefix string to be used in generating the file's name;
     *            must be at least three characters long
     * @return the created directory
     * @throws IOException
     *             if file could not be created
     */
    public static File createTempDir(String prefix) throws IOException {
        return createTempDir(prefix, null);
    }

    /**
     * Helper function to create a temp directory.
     *
     * @param prefix
     *            The prefix string to be used in generating the file's name;
     *            must be at least three characters long
     * @param parentDir
     *            The parent directory in which the directory is to be created.
     *            If <code>null</code> the system default temp directory will be
     *            used.
     * @return the created directory
     * @throws IOException
     *             if file could not be created
     */
    public static File createTempDir(String prefix, File parentDir) throws IOException {
        // create a temp file with unique name, then make it a directory
        File tmpDir = File.createTempFile(prefix, "", parentDir);
        tmpDir.delete();
        if (!tmpDir.mkdirs()) {
            throw new IOException("unable to create directory");
        }
        return tmpDir;
    }

    /**
     * Helper wrapper function around
     * {@link File#createTempFile(String, String)} that audits for potential out
     * of disk space scenario.
     *
     * @see {@link File#createTempFile(String, String)}
     * @throws LowDiskSpaceException
     *             if disk space on temporary partition is lower than minimum
     *             allowed
     */
    public static File createTempFile(String prefix, String suffix) throws IOException {
        File returnFile = File.createTempFile(prefix, suffix);
        verifyDiskSpace(returnFile);
        return returnFile;
    }

    /**
     * Helper wrapper function around {@link File#createTempFile(String, String,
     * File parentDir)} that audits for potential out of disk space scenario.
     *
     * @see {@link File#createTempFile(String, String, File)}
     * @throws LowDiskSpaceException
     *             if disk space on partition is lower than minimum allowed
     */
    public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException {
        File returnFile = File.createTempFile(prefix, suffix, parentDir);
        verifyDiskSpace(returnFile);
        return returnFile;
    }

    /**
     * A helper method that hardlinks a file to another file
     *
     * @param origFile
     *            the original file
     * @param destFile
     *            the destination file
     * @throws IOException
     *             if failed to hardlink file
     */
    public static void hardlinkFile(File origFile, File destFile) throws IOException {
        if (!origFile.exists()) {
            throw new IOException(
                    String.format("Cannot hardlink %s. File does not exist", origFile.getAbsolutePath()));
        }
        // `ln src dest` will create a hardlink (note: not `ln -s src dest`,
        // which creates symlink)
        // note that this will fail across filesystem boundaries
        // FIXME: should probably just fall back to normal copy if this fails
        CommandResult result = RunUtil.getDefault().runTimedCmd(10 * 1000, "ln", origFile.getAbsolutePath(),
                destFile.getAbsolutePath());
        if (!result.getStatus().equals(CommandStatus.SUCCESS)) {
            throw new IOException(String.format("Failed to hardlink %s to %s.  Across filesystem boundary?",
                    origFile.getAbsolutePath(), destFile.getAbsolutePath()));
        }
    }

    /**
     * Recursively hardlink folder contents.
     * <p/>
     * Only supports copying of files and directories - symlinks are not copied.
     *
     * @param sourceDir
     *            the folder that contains the files to copy
     * @param destDir
     *            the destination folder
     * @throws IOException
     */
    public static void recursiveHardlink(File sourceDir, File destDir) throws IOException {
        for (File childFile : sourceDir.listFiles()) {
            File destChild = new File(destDir, childFile.getName());
            if (childFile.isDirectory()) {
                if (!destChild.mkdir()) {
                    throw new IOException(
                            String.format("Could not create directory %s", destChild.getAbsolutePath()));
                }
                recursiveHardlink(childFile, destChild);
            } else if (childFile.isFile()) {
                hardlinkFile(childFile, destChild);
            }
        }
    }

    /**
     * A helper method that copies a file's contents to a local file
     *
     * @param origFile
     *            the original file to be copied
     * @param destFile
     *            the destination file
     * @throws IOException
     *             if failed to copy file
     */
    public static void copyFile(File origFile, File destFile) throws IOException {
        writeToFile(new FileInputStream(origFile), destFile);
    }

    public static void copyFileToDir(File origFile, File destDir) throws IOException {
        FileUtil.copyFile(origFile, new File(destDir, origFile.getName()));
    }

    /**
     * Recursively copy folder contents.
     * <p/>
     * Only supports copying of files and directories - symlinks are not copied.
     *
     * @param sourceDir
     *            the folder that contains the files to copy
     * @param destDir
     *            the destination folder
     * @throws IOException
     */
    public static void recursiveCopy(File sourceDir, File destDir) throws IOException {
        File[] childFiles = sourceDir.listFiles();
        if (childFiles == null) {
            throw new IOException(
                    String.format("Failed to recursively copy. Could not determine contents for directory '%s'",
                            sourceDir.getAbsolutePath()));
        }
        for (File childFile : childFiles) {
            File destChild = new File(destDir, childFile.getName());
            if (childFile.isDirectory()) {
                if (!destChild.mkdir()) {
                    throw new IOException(
                            String.format("Could not create directory %s", destChild.getAbsolutePath()));
                }
                recursiveCopy(childFile, destChild);
            } else if (childFile.isFile()) {
                copyFile(childFile, destChild);
            }
        }
    }

    /**
     * A helper method for reading string data from a file
     *
     * @param sourceFile
     *            the file to read from
     * @throws IOException
     * @throws FileNotFoundException
     */
    public static String readStringFromFile(File sourceFile, String charset) throws IOException {
        FileInputStream is = null;
        try {
            // no need to buffer since StreamUtil does
            is = new FileInputStream(sourceFile);
            return StreamUtil.getStringFromStream(is, charset);
        } finally {
            StreamUtil.close(is);
        }
    }

    public static String readStringFromFile(File sourceFile) throws IOException {
        FileInputStream is = null;
        try {
            // no need to buffer since StreamUtil does
            is = new FileInputStream(sourceFile);
            return StreamUtil.getStringFromStream(is);
        } finally {
            StreamUtil.close(is);
        }
    }

    /**
     * A helper method for writing string data to file
     *
     * @param inputString
     *            the input {@link String}
     * @param destFile
     *            the dest file to write to
     */
    public static void writeToFile(String inputString, File destFile) throws IOException {
        writeToFile(new ByteArrayInputStream(inputString.getBytes()), destFile);
    }

    /**
     * A helper method for writing stream data to file
     *
     * @param input
     *            the unbuffered input stream
     * @param destFile
     *            the dest file to write to
     */
    public static void writeToFile(InputStream input, File destFile) throws IOException {
        InputStream origStream = null;
        OutputStream destStream = null;
        try {
            origStream = new BufferedInputStream(input);
            destStream = new BufferedOutputStream(new FileOutputStream(destFile, true));
            StreamUtil.copyStreams(origStream, destStream);
        } finally {
            StreamUtil.close(origStream);
            StreamUtil.close(destStream);
        }
    }

    private static void verifyDiskSpace(File file) {
        // Based on empirical testing File.getUsableSpace is a low cost
        // operation (~ 100 us for
        // local disk, ~ 100 ms for network disk). Therefore call it every time
        // tmp file is
        // created
        if (file.getUsableSpace() < MIN_DISK_SPACE) {
            throw new LowDiskSpaceException(String.format("Available space on %s is less than %s MB",
                    file.getAbsolutePath(), MIN_DISK_SPACE_MB));
        }
    }

    /**
     * Recursively delete given file and all its contents
     */
    public static void recursiveDelete(File rootDir) {
        if (rootDir.isDirectory()) {
            File[] childFiles = rootDir.listFiles();
            if (childFiles != null) {
                for (File child : childFiles) {
                    recursiveDelete(child);
                }
            }
        }
        rootDir.delete();
    }

    /**
     * Utility method to extract entire contents of zip file into given
     * directory
     *
     * @param zipFile
     *            the {@link ZipFile} to extract
     * @param destDir
     *            the local dir to extract file to
     * @throws IOException
     *             if failed to extract file
     */
    public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
        Enumeration<? extends ZipEntry> entries = zipFile.entries();
        while (entries.hasMoreElements()) {

            ZipEntry entry = entries.nextElement();
            File childFile = new File(destDir, entry.getName());
            childFile.getParentFile().mkdirs();
            if (entry.isDirectory()) {
                continue;
            } else {
                FileUtil.writeToFile(zipFile.getInputStream(entry), childFile);
            }
        }
    }

    public static void extractTarGzip(File tarGzipFile, File destDir)
            throws FileNotFoundException, IOException, ArchiveException {
        GZIPInputStream gzipIn = null;
        ArchiveInputStream archivIn = null;
        BufferedInputStream buffIn = null;
        BufferedOutputStream buffOut = null;
        try {
            gzipIn = new GZIPInputStream(new BufferedInputStream(new FileInputStream(tarGzipFile)));
            archivIn = new ArchiveStreamFactory().createArchiveInputStream("tar", gzipIn);
            buffIn = new BufferedInputStream(archivIn);
            TarArchiveEntry entry = null;
            while ((entry = (TarArchiveEntry) archivIn.getNextEntry()) != null) {
                String entryName = entry.getName();
                String[] engtryPart = entryName.split("/");
                StringBuilder fullPath = new StringBuilder();
                fullPath.append(destDir.getAbsolutePath());
                for (String e : engtryPart) {
                    fullPath.append(File.separator);
                    fullPath.append(e);
                }
                File destFile = new File(fullPath.toString());
                if (entryName.endsWith("/")) {
                    if (!destFile.exists())
                        destFile.mkdirs();
                } else {
                    if (!destFile.exists())
                        destFile.createNewFile();
                    buffOut = new BufferedOutputStream(new FileOutputStream(destFile));
                    byte[] buf = new byte[8192];
                    int len = 0;
                    while ((len = buffIn.read(buf)) != -1) {
                        buffOut.write(buf, 0, len);
                    }
                    buffOut.flush();
                }
            }
        } finally {
            if (buffOut != null) {
                buffOut.close();
            }
            if (buffIn != null) {
                buffIn.close();
            }
            if (archivIn != null) {
                archivIn.close();
            }
            if (gzipIn != null) {
                gzipIn.close();
            }
        }

    }

    public static void extractGzip(File tarGzipFile, File destDir)
            throws FileNotFoundException, IOException, ArchiveException {
        String srcGzipName = tarGzipFile.getName();
        String srcUnzipName = srcGzipName.substring(0, srcGzipName.length() - 3);
        extractGzip(tarGzipFile, destDir, srcUnzipName);
    }

    public static void extractGzip(File tarGzipFile, File destDir, String destName)
            throws FileNotFoundException, IOException, ArchiveException {
        GZIPInputStream zipIn = null;
        BufferedOutputStream buffOut = null;
        try {
            File destUnzipFile = new File(destDir.getAbsolutePath(), destName);
            zipIn = new GZIPInputStream(new FileInputStream(tarGzipFile));
            buffOut = new BufferedOutputStream(new FileOutputStream(destUnzipFile));
            int b = 0;
            byte[] buf = new byte[8192];
            while ((b = zipIn.read(buf)) != -1) {
                buffOut.write(buf, 0, b);
            }
        } finally {
            if (zipIn != null)
                zipIn.close();
            if (buffOut != null)
                buffOut.close();
        }
    }

    /**
     * Utility method to extract one specific file from zip file into a tmp file
     *
     * @param zipFile
     *            the {@link ZipFile} to extract
     * @param filePath
     *            the filePath of to extract
     * @throws IOException
     *             if failed to extract file
     * @return the {@link File} or null if not found
     */
    public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
        ZipEntry entry = zipFile.getEntry(filePath);
        if (entry == null) {
            return null;
        }
        File createdFile = FileUtil.createTempFile("extracted", FileUtil.getExtension(filePath));
        FileUtil.writeToFile(zipFile.getInputStream(entry), createdFile);
        return createdFile;
    }

    /**
     * Utility method to create a temporary zip file containing the given
     * directory and all its contents.
     *
     * @param dir
     *            the directory to zip
     * @return a temporary zip {@link File} containing directory contents
     * @throws IOException
     *             if failed to create zip file
     */
    public static File createZip(File dir) throws IOException {
        File zipFile = FileUtil.createTempFile("dir", ".zip");
        createZip(dir, zipFile);
        return zipFile;
    }

    /**
     * Utility method to create a zip file containing the given directory and
     * all its contents.
     *
     * @param dir
     *            the directory to zip
     * @param zipFile
     *            the zip file to create - it should not already exist
     * @throws IOException
     *             if failed to create zip file
     */
    public static void createZip(File dir, File zipFile) throws IOException {
        ZipOutputStream out = null;
        try {
            FileOutputStream fileStream = new FileOutputStream(zipFile);
            out = new ZipOutputStream(new BufferedOutputStream(fileStream));
            addToZip(out, dir, new LinkedList<String>());
        } catch (IOException e) {
            zipFile.delete();
            throw e;
        } catch (RuntimeException e) {
            zipFile.delete();
            throw e;
        } finally {
            StreamUtil.close(out);
        }
    }

    /**
     * Recursively adds given file and its contents to ZipOutputStream
     *
     * @param out
     *            the {@link ZipOutputStream}
     * @param file
     *            the {@link File} to add to the stream
     * @param relativePathSegs
     *            the relative path of file, including separators
     * @throws IOException
     *             if failed to add file to zip
     */
    private static void addToZip(ZipOutputStream out, File file, List<String> relativePathSegs) throws IOException {
        relativePathSegs.add(file.getName());
        if (file.isDirectory()) {
            // note: it appears even on windows, ZipEntry expects '/' as a path
            // separator
            relativePathSegs.add("/");
        }
        ZipEntry zipEntry = new ZipEntry(buildPath(relativePathSegs));
        out.putNextEntry(zipEntry);
        if (file.isFile()) {
            writeToStream(file, out);
        }
        out.closeEntry();
        if (file.isDirectory()) {
            // recursively add contents
            File[] subFiles = file.listFiles();
            if (subFiles == null) {
                throw new IOException(String.format("Could not read directory %s", file.getAbsolutePath()));
            }
            for (File subFile : subFiles) {
                addToZip(out, subFile, relativePathSegs);
            }
            // remove the path separator
            relativePathSegs.remove(relativePathSegs.size() - 1);
        }
        // remove the last segment, added at beginning of method
        relativePathSegs.remove(relativePathSegs.size() - 1);
    }

    /**
     * Close an open {@link ZipFile}, ignoring any exceptions.
     *
     * @param otaZip
     *            the file to close
     */
    public static void closeZip(ZipFile otaZip) {
        if (otaZip != null) {
            try {
                otaZip.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }

    /**
     * Helper method to create a gzipped version of a single file.
     *
     * @param file
     *            the original file
     * @param gzipFile
     *            the file to place compressed contents in
     * @throws IOException
     */
    public static void gzipFile(File file, File gzipFile) throws IOException {
        GZIPOutputStream out = null;
        try {
            FileOutputStream fileStream = new FileOutputStream(gzipFile);
            out = new GZIPOutputStream(new BufferedOutputStream(fileStream, 64 * 1024));
            writeToStream(file, out);
        } catch (IOException e) {
            gzipFile.delete();
            throw e;
        } catch (RuntimeException e) {
            gzipFile.delete();
            throw e;
        } finally {
            StreamUtil.close(out);
        }
    }

    /**
     * Helper method to write input file contents to output stream.
     *
     * @param file
     *            the input {@link File}
     * @param out
     *            the {@link OutputStream}
     *
     * @throws IOException
     */
    private static void writeToStream(File file, OutputStream out) throws IOException {
        InputStream inputStream = null;
        try {
            inputStream = new BufferedInputStream(new FileInputStream(file));
            StreamUtil.copyStreams(inputStream, out);
        } finally {
            StreamUtil.close(inputStream);
        }
    }

    /**
     * Builds a file system path from a stack of relative path segments
     *
     * @param relativePathSegs
     *            the list of relative paths
     * @return a {@link String} containing all relativePathSegs
     */
    private static String buildPath(List<String> relativePathSegs) {
        StringBuilder pathBuilder = new StringBuilder();
        for (String segment : relativePathSegs) {
            pathBuilder.append(segment);
        }
        return pathBuilder.toString();
    }

    /**
     * Gets the extension for given file name.
     *
     * @param fileName
     * @return the extension or empty String if file has no extension
     */
    public static String getExtension(String fileName) {
        int index = fileName.lastIndexOf('.');
        if (index == -1) {
            return "";
        } else {
            return fileName.substring(index);
        }
    }

    /**
     * Gets the base name, without extension, of given file name.
     * <p/>
     * e.g. getBaseName("file.txt") will return "file"
     *
     * @param fileName
     * @return the base name
     */
    public static String getBaseName(String fileName) {
        int index = fileName.lastIndexOf('.');
        if (index == -1) {
            return fileName;
        } else {
            return fileName.substring(0, index);
        }
    }

    /**
     * Utility method to do byte-wise content comparison of two files.
     *
     * @return <code>true</code> if file contents are identical
     */
    public static boolean compareFileContents(File file1, File file2) throws IOException {
        BufferedInputStream stream1 = null;
        BufferedInputStream stream2 = null;

        boolean result = true;
        try {
            stream1 = new BufferedInputStream(new FileInputStream(file1));
            stream2 = new BufferedInputStream(new FileInputStream(file2));
            boolean eof = false;
            while (!eof) {
                int byte1 = stream1.read();
                int byte2 = stream2.read();
                if (byte1 != byte2) {
                    result = false;
                    break;
                }
                eof = byte1 == -1;
            }
        } finally {
            StreamUtil.close(stream1);
            StreamUtil.close(stream2);
        }
        return result;
    }

    /**
     * Helper method which constructs a unique file on temporary disk, whose
     * name corresponds as closely as possible to the file name given by the
     * remote file path
     *
     * @param remoteFilePath
     *            the '/' separated remote path to construct the name from
     * @param parentDir
     *            the parent directory to create the file in. <code>null</code>
     *            to use the default temporary directory
     */
    public static File createTempFileForRemote(String remoteFilePath, File parentDir) throws IOException {
        String[] segments = remoteFilePath.split("/");
        // take last segment as base name
        String remoteFileName = segments[segments.length - 1];
        String prefix = getBaseName(remoteFileName);
        if (prefix.length() < 3) {
            // prefix must be at least 3 characters long
            prefix = prefix + "XXX";
        }
        String fileExt = getExtension(remoteFileName);

        // create a unique file name. Add a underscore to prefix so file name is
        // more readable
        // e.g. myfile_57588758.img rather than myfile57588758.img
        File tmpFile = FileUtil.createTempFile(prefix + "_", fileExt, parentDir);
        return tmpFile;
    }

    /**
     * Try to delete a file and silently ignore IOExceptions. Intended for use
     * when cleaning up in {@code finally} stanzas.
     * 
     * @param file
     *            may be null.
     */
    public static void deleteFile(File file) {
        if (file != null) {
            file.delete();
        }
    }

    /**
     * Helper method to build a system-dependent File
     *
     * @param parentDir
     *            the parent directory to use.
     * @param pathSegments
     *            the relative path segments to use
     * @return the {@link File} representing given path, with each
     *         <var>pathSegment</var> separated by {@link File#separatorChar}
     */
    public static File getFileForPath(File parentDir, String... pathSegments) {
        return new File(parentDir, getPath(pathSegments));
    }

    /**
     * Helper method to build a system-dependent relative path
     *
     * @param pathSegments
     *            the relative path segments to use
     * @return the {@link String} representing given path, with each
     *         <var>pathSegment</var> separated by {@link File#separatorChar}
     */
    public static String getPath(String... pathSegments) {
        StringBuilder pathBuilder = new StringBuilder();
        boolean isFirst = true;
        for (String path : pathSegments) {
            if (!isFirst) {
                pathBuilder.append(File.separatorChar);
            } else {
                isFirst = false;
            }
            pathBuilder.append(path);
        }
        return pathBuilder.toString();
    }

    /**
     * Recursively search given directory for first file with given name
     *
     * @param dir
     *            the directory to search
     * @param fileName
     *            the name of the file to search for
     * @return the {@link File} or <code>null</code> if it could not be found
     */
    public static File findFile(File dir, String fileName) {
        if (dir.listFiles() != null) {
            for (File file : dir.listFiles()) {
                if (file.getName().equals(fileName)) {
                    return file;
                } else if (file.isDirectory()) {
                    File result = findFile(file, fileName);
                    if (result != null) {
                        return result;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Recursively find all directories under the given {@code rootDir}
     *
     * @param rootDir
     *            the root directory to search in
     * @param relativeParent
     *            An optional parent for all {@link File}s returned. If not
     *            specified, all {@link File}s will be relative to
     *            {@code rootDir}.
     * @return An set of {@link File}s, representing all directories under
     *         {@code rootDir}, including {@code rootDir} itself. If
     *         {@code rootDir} is null, an empty set is returned.
     */
    public static Set<File> findDirsUnder(File rootDir, File relativeParent) {
        Set<File> dirs = new HashSet<File>();
        if (rootDir != null) {
            if (!rootDir.isDirectory()) {
                throw new IllegalArgumentException(
                        "Can't find dirs under '" + rootDir + "'. It's not a directory.");
            }
            File thisDir = new File(relativeParent, rootDir.getName());
            dirs.add(thisDir);
            for (File file : rootDir.listFiles()) {
                if (file.isDirectory()) {
                    dirs.addAll(findDirsUnder(file, thisDir));
                }
            }
        }
        return dirs;
    }

    /**
     * Convert the given file size in bytes to a more readable format in
     * X.Y[KMGT] format.
     *
     * @param sizeLong
     *            file size in bytes
     * @return descriptive string of file size
     */
    public static String convertToReadableSize(long sizeLong) {

        double size = sizeLong;
        for (int i = 0; i < SIZE_SPECIFIERS.length; i++) {
            if (size < 1024) {
                return String.format("%.1f%c", size, SIZE_SPECIFIERS[i]);
            }
            size /= 1024f;
        }
        throw new IllegalArgumentException(
                String.format("Passed a file size of %d, I cannot count that high", size));
    }

    /**
     * The inverse of {@link #convertToReadableSize(long)}. Converts the
     * readable format described in {@link #convertToReadableSize(long)} to a
     * byte value.
     *
     * @param sizeString
     *            the string description of the size.
     * @return the size in bytes
     * @throws IllegalArgumentException
     *             if cannot recognize size
     */
    public static long convertSizeToBytes(String sizeString) throws IllegalArgumentException {
        if (sizeString.isEmpty()) {
            throw new IllegalArgumentException("invalid empty string");
        }
        char sizeSpecifier = sizeString.charAt(sizeString.length() - 1);
        long multiplier = findMultiplier(sizeSpecifier);
        try {
            String numberString = sizeString;
            if (multiplier != 1) {
                // strip off last char
                numberString = sizeString.substring(0, sizeString.length() - 1);
            }
            return multiplier * Long.parseLong(numberString);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(String.format("Unrecognized size %s", sizeString));
        }
    }

    private static long findMultiplier(char sizeSpecifier) {
        long multiplier = 1;
        for (int i = 1; i < SIZE_SPECIFIERS.length; i++) {
            multiplier *= 1024;
            if (sizeSpecifier == SIZE_SPECIFIERS[i]) {
                return multiplier;
            }
        }
        // not found
        return 1;
    }
}