Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.tradefed.util;
     17 
     18 import com.android.ddmlib.Log;
     19 import com.android.tradefed.command.FatalHostError;
     20 import com.android.tradefed.config.Option;
     21 import com.android.tradefed.log.LogUtil.CLog;
     22 import com.android.tradefed.result.LogDataType;
     23 
     24 import java.io.BufferedInputStream;
     25 import java.io.BufferedOutputStream;
     26 import java.io.ByteArrayInputStream;
     27 import java.io.File;
     28 import java.io.FileInputStream;
     29 import java.io.FileNotFoundException;
     30 import java.io.FileOutputStream;
     31 import java.io.FileWriter;
     32 import java.io.FilenameFilter;
     33 import java.io.IOException;
     34 import java.io.InputStream;
     35 import java.io.OutputStream;
     36 import java.nio.file.FileSystemException;
     37 import java.nio.file.FileVisitOption;
     38 import java.nio.file.Files;
     39 import java.nio.file.Paths;
     40 import java.nio.file.attribute.PosixFilePermission;
     41 import java.util.ArrayList;
     42 import java.util.Arrays;
     43 import java.util.EnumSet;
     44 import java.util.HashMap;
     45 import java.util.HashSet;
     46 import java.util.List;
     47 import java.util.Map;
     48 import java.util.Set;
     49 import java.util.zip.ZipFile;
     50 
     51 /**
     52  * A helper class for file related operations
     53  */
     54 public class FileUtil {
     55 
     56     private static final String LOG_TAG = "FileUtil";
     57     /**
     58      * The minimum allowed disk space in megabytes. File creation methods will throw
     59      * {@link LowDiskSpaceException} if the usable disk space in desired partition is less than
     60      * this amount.
     61      */
     62     @Option(name = "min-disk-space", description = "The minimum allowed disk"
     63         + " space in megabytes for file-creation methods. May be set to"
     64         + " 0 to disable checking.")
     65     private static long mMinDiskSpaceMb = 100;
     66 
     67     private static final char[] SIZE_SPECIFIERS = {
     68             ' ', 'K', 'M', 'G', 'T'
     69     };
     70 
     71     private static String sChmod = "chmod";
     72 
     73     /** A map of {@link PosixFilePermission} to its corresponding Unix file mode */
     74     private static final Map<PosixFilePermission, Integer> PERM_MODE_MAP = new HashMap<>();
     75     static {
     76         PERM_MODE_MAP.put(PosixFilePermission.OWNER_READ,     0b100000000);
     77         PERM_MODE_MAP.put(PosixFilePermission.OWNER_WRITE,    0b010000000);
     78         PERM_MODE_MAP.put(PosixFilePermission.OWNER_EXECUTE,  0b001000000);
     79         PERM_MODE_MAP.put(PosixFilePermission.GROUP_READ,     0b000100000);
     80         PERM_MODE_MAP.put(PosixFilePermission.GROUP_WRITE,    0b000010000);
     81         PERM_MODE_MAP.put(PosixFilePermission.GROUP_EXECUTE,  0b000001000);
     82         PERM_MODE_MAP.put(PosixFilePermission.OTHERS_READ,    0b000000100);
     83         PERM_MODE_MAP.put(PosixFilePermission.OTHERS_WRITE,   0b000000010);
     84         PERM_MODE_MAP.put(PosixFilePermission.OTHERS_EXECUTE, 0b000000001);
     85     }
     86 
     87     public static final int FILESYSTEM_FILENAME_MAX_LENGTH = 255;
     88 
     89     /**
     90      * Exposed for testing. Allows to modify the chmod binary name we look for, in order to tests
     91      * system with no chmod support.
     92      */
     93     protected static void setChmodBinary(String chmodName) {
     94         sChmod = chmodName;
     95     }
     96 
     97     /**
     98      * Thrown if usable disk space is below minimum threshold.
     99      */
    100     @SuppressWarnings("serial")
    101     public static class LowDiskSpaceException extends FatalHostError {
    102 
    103         LowDiskSpaceException(String msg, Throwable cause) {
    104             super(msg, cause);
    105         }
    106 
    107         LowDiskSpaceException(String msg) {
    108             super(msg);
    109         }
    110 
    111     }
    112 
    113     /**
    114      * Method to create a chain of directories, and set them all group execute/read/writable as they
    115      * are created, by calling {@link #chmodGroupRWX(File)}.  Essentially a version of
    116      * {@link File#mkdirs()} that also runs {@link #chmod(File, String)}.
    117      *
    118      * @param file the name of the directory to create, possibly with containing directories that
    119      *        don't yet exist.
    120      * @return {@code true} if {@code file} exists and is a directory, {@code false} otherwise.
    121      */
    122     public static boolean mkdirsRWX(File file) {
    123         File parent = file.getParentFile();
    124 
    125         if (parent != null && !parent.isDirectory()) {
    126             // parent doesn't exist.  recurse upward, which should both mkdir and chmod
    127             if (!mkdirsRWX(parent)) {
    128                 // Couldn't mkdir parent, fail
    129                 Log.w(LOG_TAG, String.format("Failed to mkdir parent dir %s.", parent));
    130                 return false;
    131             }
    132         }
    133 
    134         // by this point the parent exists.  Try to mkdir file
    135         if (file.isDirectory() || file.mkdir()) {
    136             // file should exist.  Try chmod and complain if that fails, but keep going
    137             boolean setPerms = chmodGroupRWX(file);
    138             if (!setPerms) {
    139                 Log.w(LOG_TAG, String.format("Failed to set dir %s to be group accessible.", file));
    140             }
    141         }
    142 
    143         return file.isDirectory();
    144     }
    145 
    146     public static boolean chmodRWXRecursively(File file) {
    147         boolean success = true;
    148         if (!file.setExecutable(true, false)) {
    149             CLog.w("Failed to set %s executable.", file.getAbsolutePath());
    150             success = false;
    151         }
    152         if (!file.setWritable(true, false)) {
    153             CLog.w("Failed to set %s writable.", file.getAbsolutePath());
    154             success = false;
    155         }
    156         if (!file.setReadable(true, false)) {
    157             CLog.w("Failed to set %s readable", file.getAbsolutePath());
    158             success = false;
    159         }
    160 
    161         if (file.isDirectory()) {
    162             File[] children = file.listFiles();
    163             for (File child : children) {
    164                 if (!chmodRWXRecursively(child)) {
    165                     success = false;
    166                 }
    167             }
    168 
    169         }
    170         return success;
    171     }
    172 
    173     public static boolean chmod(File file, String perms) {
    174         Log.d(LOG_TAG, String.format("Attempting to chmod %s to %s",
    175                 file.getAbsolutePath(), perms));
    176         CommandResult result =
    177                 RunUtil.getDefault().runTimedCmd(10 * 1000, sChmod, perms, file.getAbsolutePath());
    178         return result.getStatus().equals(CommandStatus.SUCCESS);
    179     }
    180 
    181     /**
    182      * Performs a best effort attempt to make given file group readable and writable.
    183      * <p/>
    184      * Note that the execute permission is required to make directories accessible.  See
    185      * {@link #chmodGroupRWX(File)}.
    186      * <p/>
    187      * If 'chmod' system command is not supported by underlying OS, will set file to writable by
    188      * all.
    189      *
    190      * @param file the {@link File} to make owner and group writable
    191      * @return <code>true</code> if file was successfully made group writable, <code>false</code>
    192      *         otherwise
    193      */
    194     public static boolean chmodGroupRW(File file) {
    195         if (chmodExists()) {
    196             if (chmod(file, "ug+rw")) {
    197                 return true;
    198             } else {
    199                 Log.d(LOG_TAG, String.format("Failed chmod on %s", file.getAbsolutePath()));
    200                 return false;
    201             }
    202         } else {
    203             Log.d(LOG_TAG, String.format("chmod not available; "
    204                     + "attempting to set %s globally RW", file.getAbsolutePath()));
    205             return file.setWritable(true, false /* false == writable for all */) &&
    206                     file.setReadable(true, false /* false == readable for all */);
    207         }
    208     }
    209 
    210     /**
    211      * Performs a best effort attempt to make given file group executable, readable, and writable.
    212      * <p/>
    213      * If 'chmod' system command is not supported by underlying OS, will attempt to set permissions
    214      * for all users.
    215      *
    216      * @param file the {@link File} to make owner and group writable
    217      * @return <code>true</code> if permissions were set successfully, <code>false</code> otherwise
    218      */
    219     public static boolean chmodGroupRWX(File file) {
    220         if (chmodExists()) {
    221             if (chmod(file, "ug+rwx")) {
    222                 return true;
    223             } else {
    224                 Log.d(LOG_TAG, String.format("Failed chmod on %s", file.getAbsolutePath()));
    225                 return false;
    226             }
    227         } else {
    228             Log.d(LOG_TAG, String.format("chmod not available; "
    229                     + "attempting to set %s globally RWX", file.getAbsolutePath()));
    230             return file.setExecutable(true, false /* false == executable for all */) &&
    231                     file.setWritable(true, false /* false == writable for all */) &&
    232                     file.setReadable(true, false /* false == readable for all */);
    233         }
    234     }
    235 
    236     /**
    237      * Internal helper to determine if 'chmod' is available on the system OS.
    238      */
    239     protected static boolean chmodExists() {
    240         // Silence the scary process exception when chmod is missing, we will log instead.
    241         CommandResult result = RunUtil.getDefault().runTimedCmdSilently(10 * 1000, sChmod);
    242         // We expect a status fail because 'chmod' requires arguments.
    243         String stderr = result.getStderr();
    244         if (CommandStatus.FAILED.equals(result.getStatus()) &&
    245                 (stderr.contains("chmod: missing operand") || stderr.contains("usage: "))) {
    246             return true;
    247         }
    248         CLog.w("Chmod is not supported by this OS.");
    249         return false;
    250     }
    251 
    252     /**
    253      * Recursively set read and exec (if folder) permissions for given file.
    254      */
    255     public static void setReadableRecursive(File file) {
    256         file.setReadable(true);
    257         if (file.isDirectory()) {
    258             file.setExecutable(true);
    259             File[] children = file.listFiles();
    260             if (children != null) {
    261                 for (File childFile : file.listFiles()) {
    262                     setReadableRecursive(childFile);
    263                 }
    264             }
    265         }
    266     }
    267 
    268     /**
    269      * Helper function to create a temp directory in the system default temporary file directory.
    270      *
    271      * @param prefix The prefix string to be used in generating the file's name; must be at least
    272      *            three characters long
    273      * @return the created directory
    274      * @throws IOException if file could not be created
    275      */
    276     public static File createTempDir(String prefix) throws IOException {
    277         return createTempDir(prefix, null);
    278     }
    279 
    280     /**
    281      * Helper function to create a temp directory.
    282      *
    283      * @param prefix The prefix string to be used in generating the file's name; must be at least
    284      *            three characters long
    285      * @param parentDir The parent directory in which the directory is to be created. If
    286      *            <code>null</code> the system default temp directory will be used.
    287      * @return the created directory
    288      * @throws IOException if file could not be created
    289      */
    290     public static File createTempDir(String prefix, File parentDir) throws IOException {
    291         // create a temp file with unique name, then make it a directory
    292         if (parentDir != null) {
    293             CLog.d("Creating temp directory at %s with prefix \"%s\"",
    294               parentDir.getAbsolutePath(), prefix);
    295         }
    296         File tmpDir = File.createTempFile(prefix, "", parentDir);
    297         return deleteFileAndCreateDirWithSameName(tmpDir);
    298     }
    299 
    300     private static File deleteFileAndCreateDirWithSameName(File tmpDir) throws IOException {
    301         tmpDir.delete();
    302         return createDir(tmpDir);
    303     }
    304 
    305     private static File createDir(File tmpDir) throws IOException {
    306         if (!tmpDir.mkdirs()) {
    307             throw new IOException("unable to create directory");
    308         }
    309         return tmpDir;
    310     }
    311 
    312     /**
    313      * Helper function to create a named directory inside your temp folder.
    314      * <p/>
    315      * This directory will not have it's name randomized. If the directory already exists it will
    316      * be returned.
    317      *
    318      * @param name The name of the directory to create in your tmp folder.
    319      * @return the created directory
    320      */
    321     public static File createNamedTempDir(String name) throws IOException {
    322         File namedTmpDir = new File(System.getProperty("java.io.tmpdir"), name);
    323         if (!namedTmpDir.exists()) {
    324             createDir(namedTmpDir);
    325         }
    326         return namedTmpDir;
    327     }
    328 
    329     /**
    330      * Helper wrapper function around {@link File#createTempFile(String, String)} that audits for
    331      * potential out of disk space scenario.
    332      *
    333      * @see File#createTempFile(String, String)
    334      * @throws LowDiskSpaceException if disk space on temporary partition is lower than minimum
    335      *             allowed
    336      */
    337     public static File createTempFile(String prefix, String suffix) throws IOException {
    338         return internalCreateTempFile(prefix, suffix, null);
    339     }
    340 
    341     /**
    342      * Helper wrapper function around {@link File#createTempFile(String, String, File)}
    343      * that audits for potential out of disk space scenario.
    344      *
    345      * @see File#createTempFile(String, String, File)
    346      * @throws LowDiskSpaceException if disk space on partition is lower than minimum allowed
    347      */
    348     public static File createTempFile(String prefix, String suffix, File parentDir)
    349             throws IOException {
    350         return internalCreateTempFile(prefix, suffix, parentDir);
    351     }
    352 
    353     /**
    354      * Internal helper to create a temporary file.
    355      */
    356     private static File internalCreateTempFile(String prefix, String suffix, File parentDir)
    357             throws IOException {
    358         // File.createTempFile add an additional random long in the name so we remove the length.
    359         int overflowLength = prefix.length() + 19 - FILESYSTEM_FILENAME_MAX_LENGTH;
    360         if (suffix != null) {
    361             // suffix may be null
    362             overflowLength += suffix.length();
    363         }
    364         if (overflowLength > 0) {
    365             CLog.w("Filename for prefix: %s and suffix: %s, would be too long for FileSystem,"
    366                     + "truncating it.", prefix, suffix);
    367             // We truncate from suffix in priority because File.createTempFile wants prefix to be
    368             // at least 3 characters.
    369             if (suffix.length() >= overflowLength) {
    370                 int temp = overflowLength;
    371                 overflowLength -= suffix.length();
    372                 suffix = suffix.substring(temp, suffix.length());
    373             } else {
    374                 overflowLength -= suffix.length();
    375                 suffix = "";
    376             }
    377             if (overflowLength > 0) {
    378                 // Whatever remaining to remove after suffix has been truncating should be inside
    379                 // prefix, otherwise there would not be overflow.
    380                 prefix = prefix.substring(0, prefix.length() - overflowLength);
    381             }
    382         }
    383         File returnFile = null;
    384         if (parentDir != null) {
    385             CLog.d("Creating temp file at %s with prefix \"%s\" suffix \"%s\"",
    386                     parentDir.getAbsolutePath(), prefix, suffix);
    387         }
    388         returnFile = File.createTempFile(prefix, suffix, parentDir);
    389         verifyDiskSpace(returnFile);
    390         return returnFile;
    391     }
    392 
    393     /**
    394      * A helper method that hardlinks a file to another file. Fallback to copy in case of cross
    395      * partition linking.
    396      *
    397      * @param origFile the original file
    398      * @param destFile the destination file
    399      * @throws IOException if failed to hardlink file
    400      */
    401     public static void hardlinkFile(File origFile, File destFile) throws IOException {
    402         try {
    403             Files.createLink(destFile.toPath(), origFile.toPath());
    404         } catch (FileSystemException e) {
    405             if (e.getMessage().contains("Invalid cross-device link")) {
    406                 CLog.d("Hardlink failed: '%s', falling back to copy.", e.getMessage());
    407                 copyFile(origFile, destFile);
    408                 return;
    409             }
    410             throw e;
    411         }
    412     }
    413 
    414     /**
    415      * A helper method that symlinks a file to another file
    416      *
    417      * @param origFile the original file
    418      * @param destFile the destination file
    419      * @throws IOException if failed to symlink file
    420      */
    421     public static void symlinkFile(File origFile, File destFile) throws IOException {
    422         CLog.d(
    423                 "Attempting symlink from %s to %s",
    424                 origFile.getAbsolutePath(), destFile.getAbsolutePath());
    425         Files.createSymbolicLink(destFile.toPath(), origFile.toPath());
    426     }
    427 
    428     /**
    429      * Recursively hardlink folder contents.
    430      * <p/>
    431      * Only supports copying of files and directories - symlinks are not copied. If the destination
    432      * directory does not exist, it will be created.
    433      *
    434      * @param sourceDir the folder that contains the files to copy
    435      * @param destDir the destination folder
    436      * @throws IOException
    437      */
    438     public static void recursiveHardlink(File sourceDir, File destDir) throws IOException {
    439         if (!destDir.isDirectory() && !destDir.mkdir()) {
    440             throw new IOException(String.format("Could not create directory %s",
    441                     destDir.getAbsolutePath()));
    442         }
    443         for (File childFile : sourceDir.listFiles()) {
    444             File destChild = new File(destDir, childFile.getName());
    445             if (childFile.isDirectory()) {
    446                 recursiveHardlink(childFile, destChild);
    447             } else if (childFile.isFile()) {
    448                 hardlinkFile(childFile, destChild);
    449             }
    450         }
    451     }
    452 
    453     /**
    454      * Recursively symlink folder contents.
    455      *
    456      * <p>Only supports copying of files and directories - symlinks are not copied. If the
    457      * destination directory does not exist, it will be created.
    458      *
    459      * @param sourceDir the folder that contains the files to copy
    460      * @param destDir the destination folder
    461      * @throws IOException
    462      */
    463     public static void recursiveSymlink(File sourceDir, File destDir) throws IOException {
    464         if (!destDir.isDirectory() && !destDir.mkdir()) {
    465             throw new IOException(
    466                     String.format("Could not create directory %s", destDir.getAbsolutePath()));
    467         }
    468         for (File childFile : sourceDir.listFiles()) {
    469             File destChild = new File(destDir, childFile.getName());
    470             if (childFile.isDirectory()) {
    471                 recursiveSymlink(childFile, destChild);
    472             } else if (childFile.isFile()) {
    473                 symlinkFile(childFile, destChild);
    474             }
    475         }
    476     }
    477 
    478     /**
    479      * A helper method that copies a file's contents to a local file
    480      *
    481      * @param origFile the original file to be copied
    482      * @param destFile the destination file
    483      * @throws IOException if failed to copy file
    484      */
    485     public static void copyFile(File origFile, File destFile) throws IOException {
    486         writeToFile(new FileInputStream(origFile), destFile);
    487     }
    488 
    489     /**
    490      * Recursively copy folder contents.
    491      * <p/>
    492      * Only supports copying of files and directories - symlinks are not copied. If the destination
    493      * directory does not exist, it will be created.
    494      *
    495      * @param sourceDir the folder that contains the files to copy
    496      * @param destDir the destination folder
    497      * @throws IOException
    498      */
    499     public static void recursiveCopy(File sourceDir, File destDir) throws IOException {
    500         File[] childFiles = sourceDir.listFiles();
    501         if (childFiles == null) {
    502             throw new IOException(String.format(
    503                     "Failed to recursively copy. Could not determine contents for directory '%s'",
    504                     sourceDir.getAbsolutePath()));
    505         }
    506         if (!destDir.isDirectory() && !destDir.mkdir()) {
    507             throw new IOException(String.format("Could not create directory %s",
    508                 destDir.getAbsolutePath()));
    509         }
    510         for (File childFile : childFiles) {
    511             File destChild = new File(destDir, childFile.getName());
    512             if (childFile.isDirectory()) {
    513                 recursiveCopy(childFile, destChild);
    514             } else if (childFile.isFile()) {
    515                 copyFile(childFile, destChild);
    516             }
    517         }
    518     }
    519 
    520     /**
    521      * A helper method for reading string data from a file
    522      *
    523      * @param sourceFile the file to read from
    524      * @throws IOException
    525      * @throws FileNotFoundException
    526      */
    527     public static String readStringFromFile(File sourceFile) throws IOException {
    528         FileInputStream is = null;
    529         try {
    530             // no need to buffer since StreamUtil does
    531             is = new FileInputStream(sourceFile);
    532             return StreamUtil.getStringFromStream(is);
    533         } finally {
    534             StreamUtil.close(is);
    535         }
    536     }
    537 
    538     /**
    539      * A helper method for writing string data to file
    540      *
    541      * @param inputString the input {@link String}
    542      * @param destFile the destination file to write to
    543      */
    544     public static void writeToFile(String inputString, File destFile) throws IOException {
    545         writeToFile(inputString, destFile, false);
    546     }
    547 
    548     /**
    549      * A helper method for writing or appending string data to file
    550      *
    551      * @param inputString the input {@link String}
    552      * @param destFile the destination file to write or append to
    553      * @param append append to end of file if true, overwrite otherwise
    554      */
    555     public static void writeToFile(String inputString, File destFile, boolean append)
    556             throws IOException {
    557         writeToFile(new ByteArrayInputStream(inputString.getBytes()), destFile, append);
    558     }
    559 
    560     /**
    561      * A helper method for writing stream data to file
    562      *
    563      * @param input the unbuffered input stream
    564      * @param destFile the destination file to write to
    565      */
    566     public static void writeToFile(InputStream input, File destFile) throws IOException {
    567         writeToFile(input, destFile, false);
    568     }
    569 
    570     /**
    571      * A helper method for writing stream data to file
    572      *
    573      * @param input the unbuffered input stream
    574      * @param destFile the destination file to write or append to
    575      * @param append append to end of file if true, overwrite otherwise
    576      */
    577     public static void writeToFile(
    578             InputStream input, File destFile, boolean append) throws IOException {
    579         InputStream origStream = null;
    580         OutputStream destStream = null;
    581         try {
    582             origStream = new BufferedInputStream(input);
    583             destStream = new BufferedOutputStream(new FileOutputStream(destFile, append));
    584             StreamUtil.copyStreams(origStream, destStream);
    585         } finally {
    586             StreamUtil.close(origStream);
    587             StreamUtil.flushAndCloseStream(destStream);
    588         }
    589     }
    590 
    591     /**
    592      * Note: We should never use CLog in here, since it also relies on that method, this would lead
    593      * to infinite recursion.
    594      */
    595     private static void verifyDiskSpace(File file) {
    596         // Based on empirical testing File.getUsableSpace is a low cost operation (~ 100 us for
    597         // local disk, ~ 100 ms for network disk). Therefore call it every time tmp file is
    598         // created
    599         long usableSpace = 0L;
    600         File toCheck = file;
    601         if (!file.isDirectory() && file.getParentFile() != null) {
    602             // If the given file is not a directory it might not work properly so using the parent
    603             // in that case.
    604             toCheck = file.getParentFile();
    605         }
    606         usableSpace = toCheck.getUsableSpace();
    607 
    608         long minDiskSpace = mMinDiskSpaceMb * 1024 * 1024;
    609         if (usableSpace < minDiskSpace) {
    610             String message =
    611                     String.format(
    612                             "Available space on %s is %.2f MB. Min is %d MB.",
    613                             toCheck.getAbsolutePath(),
    614                             usableSpace / (1024.0 * 1024.0),
    615                             mMinDiskSpaceMb);
    616             throw new LowDiskSpaceException(message);
    617         }
    618     }
    619 
    620     /**
    621      * Recursively delete given file or directory and all its contents.
    622      *
    623      * @param rootDir the directory or file to be deleted; can be null
    624      */
    625     public static void recursiveDelete(File rootDir) {
    626         if (rootDir != null) {
    627             // We expand directories if they are not symlink
    628             if (rootDir.isDirectory() && !Files.isSymbolicLink(rootDir.toPath())) {
    629                 File[] childFiles = rootDir.listFiles();
    630                 if (childFiles != null) {
    631                     for (File child : childFiles) {
    632                         recursiveDelete(child);
    633                     }
    634                 }
    635             }
    636             rootDir.delete();
    637         }
    638     }
    639 
    640     /**
    641      * Gets the extension for given file name.
    642      *
    643      * @param fileName
    644      * @return the extension or empty String if file has no extension
    645      */
    646     public static String getExtension(String fileName) {
    647         int index = fileName.lastIndexOf('.');
    648         if (index == -1) {
    649             return "";
    650         } else {
    651             return fileName.substring(index);
    652         }
    653     }
    654 
    655     /**
    656      * Gets the base name, without extension, of given file name.
    657      * <p/>
    658      * e.g. getBaseName("file.txt") will return "file"
    659      *
    660      * @param fileName
    661      * @return the base name
    662      */
    663     public static String getBaseName(String fileName) {
    664         int index = fileName.lastIndexOf('.');
    665         if (index == -1) {
    666             return fileName;
    667         } else {
    668             return fileName.substring(0, index);
    669         }
    670     }
    671 
    672     /**
    673      * Utility method to do byte-wise content comparison of two files.
    674      *
    675      * @return <code>true</code> if file contents are identical
    676      */
    677     public static boolean compareFileContents(File file1, File file2) throws IOException {
    678         BufferedInputStream stream1 = null;
    679         BufferedInputStream stream2 = null;
    680 
    681         boolean result = true;
    682         try {
    683             stream1 = new BufferedInputStream(new FileInputStream(file1));
    684             stream2 = new BufferedInputStream(new FileInputStream(file2));
    685             boolean eof = false;
    686             while (!eof) {
    687                 int byte1 = stream1.read();
    688                 int byte2 = stream2.read();
    689                 if (byte1 != byte2) {
    690                     result = false;
    691                     break;
    692                 }
    693                 eof = byte1 == -1;
    694             }
    695         } finally {
    696             StreamUtil.close(stream1);
    697             StreamUtil.close(stream2);
    698         }
    699         return result;
    700     }
    701 
    702     /**
    703      * Helper method which constructs a unique file on temporary disk, whose name corresponds as
    704      * closely as possible to the file name given by the remote file path
    705      *
    706      * @param remoteFilePath the '/' separated remote path to construct the name from
    707      * @param parentDir the parent directory to create the file in. <code>null</code> to use the
    708      * default temporary directory
    709      */
    710     public static File createTempFileForRemote(String remoteFilePath, File parentDir)
    711             throws IOException {
    712         String[] segments = remoteFilePath.split("/");
    713         // take last segment as base name
    714         String remoteFileName = segments[segments.length - 1];
    715         String prefix = getBaseName(remoteFileName);
    716         if (prefix.length() < 3) {
    717             // prefix must be at least 3 characters long
    718             prefix = prefix + "XXX";
    719         }
    720         String fileExt = getExtension(remoteFileName);
    721 
    722         // create a unique file name. Add a underscore to prefix so file name is more readable
    723         // e.g. myfile_57588758.img rather than myfile57588758.img
    724         File tmpFile = FileUtil.createTempFile(prefix + "_", fileExt, parentDir);
    725         return tmpFile;
    726     }
    727 
    728     /**
    729      * Try to delete a file. Intended for use when cleaning up
    730      * in {@code finally} stanzas.
    731      *
    732      * @param file may be null.
    733      */
    734     public static void deleteFile(File file) {
    735         if (file != null) {
    736             file.delete();
    737         }
    738     }
    739 
    740     /**
    741      * Helper method to build a system-dependent File
    742      *
    743      * @param parentDir the parent directory to use.
    744      * @param pathSegments the relative path segments to use
    745      * @return the {@link File} representing given path, with each <var>pathSegment</var>
    746      *         separated by {@link File#separatorChar}
    747      */
    748     public static File getFileForPath(File parentDir, String... pathSegments) {
    749         return new File(parentDir, getPath(pathSegments));
    750     }
    751 
    752     /**
    753      * Helper method to build a system-dependent relative path
    754      *
    755      * @param pathSegments the relative path segments to use
    756      * @return the {@link String} representing given path, with each <var>pathSegment</var>
    757      *         separated by {@link File#separatorChar}
    758      */
    759     public static String getPath(String... pathSegments) {
    760         StringBuilder pathBuilder = new StringBuilder();
    761         boolean isFirst = true;
    762         for (String path : pathSegments) {
    763             if (!isFirst) {
    764                 pathBuilder.append(File.separatorChar);
    765             } else {
    766                 isFirst = false;
    767             }
    768             pathBuilder.append(path);
    769         }
    770         return pathBuilder.toString();
    771     }
    772 
    773     /**
    774      * Recursively search given directory for first file with given name
    775      *
    776      * @param dir the directory to search
    777      * @param fileName the name of the file to search for
    778      * @return the {@link File} or <code>null</code> if it could not be found
    779      */
    780     public static File findFile(File dir, String fileName) {
    781         if (dir.listFiles() != null) {
    782             for (File file : dir.listFiles()) {
    783                 if (file.isDirectory()) {
    784                     File result = findFile(file, fileName);
    785                     if (result != null) {
    786                         return result;
    787                     }
    788                 }
    789                 // after exploring the sub-dir, if the dir itself is the only match return it.
    790                 if (file.getName().equals(fileName)) {
    791                     return file;
    792                 }
    793             }
    794         }
    795         return null;
    796     }
    797 
    798     /**
    799      * Recursively find all directories under the given {@code rootDir}
    800      *
    801      * @param rootDir the root directory to search in
    802      * @param relativeParent An optional parent for all {@link File}s returned. If not specified,
    803      *            all {@link File}s will be relative to {@code rootDir}.
    804      * @return An set of {@link File}s, representing all directories under {@code rootDir},
    805      *         including {@code rootDir} itself. If {@code rootDir} is null, an empty set is
    806      *         returned.
    807      */
    808     public static Set<File> findDirsUnder(File rootDir, File relativeParent) {
    809         Set<File> dirs = new HashSet<File>();
    810         if (rootDir != null) {
    811             if (!rootDir.isDirectory()) {
    812                 throw new IllegalArgumentException("Can't find dirs under '" + rootDir
    813                         + "'. It's not a directory.");
    814             }
    815             File thisDir = new File(relativeParent, rootDir.getName());
    816             dirs.add(thisDir);
    817             for (File file : rootDir.listFiles()) {
    818                 if (file.isDirectory()) {
    819                     dirs.addAll(findDirsUnder(file, thisDir));
    820                 }
    821             }
    822         }
    823         return dirs;
    824     }
    825 
    826     /**
    827      * Convert the given file size in bytes to a more readable format in X.Y[KMGT] format.
    828      *
    829      * @param sizeLong file size in bytes
    830      * @return descriptive string of file size
    831      */
    832     public static String convertToReadableSize(long sizeLong) {
    833 
    834         double size = sizeLong;
    835         for (int i = 0; i < SIZE_SPECIFIERS.length; i++) {
    836             if (size < 1024) {
    837                 return String.format("%.1f%c", size, SIZE_SPECIFIERS[i]);
    838             }
    839             size /= 1024f;
    840         }
    841         throw new IllegalArgumentException(
    842                 String.format("Passed a file size of %.2f, I cannot count that high", size));
    843     }
    844 
    845     /**
    846      * The inverse of {@link #convertToReadableSize(long)}. Converts the readable format described
    847      * in {@link #convertToReadableSize(long)} to a byte value.
    848      *
    849      * @param sizeString the string description of the size.
    850      * @return the size in bytes
    851      * @throws IllegalArgumentException if cannot recognize size
    852      */
    853     public static long convertSizeToBytes(String sizeString) throws IllegalArgumentException {
    854         if (sizeString.isEmpty()) {
    855             throw new IllegalArgumentException("invalid empty string");
    856         }
    857         char sizeSpecifier = sizeString.charAt(sizeString.length() - 1);
    858         long multiplier = findMultiplier(sizeSpecifier);
    859         try {
    860             String numberString = sizeString;
    861             if (multiplier != 1) {
    862                 // strip off last char
    863                 numberString = sizeString.substring(0, sizeString.length() - 1);
    864             }
    865             return multiplier * Long.parseLong(numberString);
    866         } catch (NumberFormatException e) {
    867             throw new IllegalArgumentException(String.format("Unrecognized size %s", sizeString));
    868         }
    869     }
    870 
    871     private static long findMultiplier(char sizeSpecifier) {
    872         long multiplier = 1;
    873         for (int i = 1; i < SIZE_SPECIFIERS.length; i++) {
    874             multiplier *= 1024;
    875             if (sizeSpecifier == SIZE_SPECIFIERS[i]) {
    876                 return multiplier;
    877             }
    878         }
    879         // not found
    880         return 1;
    881     }
    882 
    883     /**
    884      * Returns all jar files found in given directory
    885      */
    886     public static List<File> collectJars(File dir) {
    887         List<File> list = new ArrayList<File>();
    888         File[] jarFiles = dir.listFiles(new JarFilter());
    889         if (jarFiles != null) {
    890             list.addAll(Arrays.asList(dir.listFiles(new JarFilter())));
    891         }
    892         return list;
    893     }
    894 
    895     private static class JarFilter implements FilenameFilter {
    896         /**
    897          * {@inheritDoc}
    898          */
    899         @Override
    900         public boolean accept(File dir, String name) {
    901             return name.endsWith(".jar");
    902         }
    903     }
    904 
    905 
    906     // Backwards-compatibility section
    907     /**
    908      * Utility method to extract entire contents of zip file into given directory
    909      *
    910      * @param zipFile the {@link ZipFile} to extract
    911      * @param destDir the local dir to extract file to
    912      * @throws IOException if failed to extract file
    913      * @deprecated Moved to {@link ZipUtil#extractZip(ZipFile, File)}.
    914      */
    915     @Deprecated
    916     public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
    917         ZipUtil.extractZip(zipFile, destDir);
    918     }
    919 
    920     /**
    921      * Utility method to extract one specific file from zip file into a tmp file
    922      *
    923      * @param zipFile  the {@link ZipFile} to extract
    924      * @param filePath the filePath of to extract
    925      * @return the {@link File} or null if not found
    926      * @throws IOException if failed to extract file
    927      * @deprecated Moved to {@link ZipUtil#extractFileFromZip(ZipFile, String)}.
    928      */
    929     @Deprecated
    930     public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
    931         return ZipUtil.extractFileFromZip(zipFile, filePath);
    932     }
    933 
    934     /**
    935      * Utility method to create a temporary zip file containing the given directory and
    936      * all its contents.
    937      *
    938      * @param dir the directory to zip
    939      * @return a temporary zip {@link File} containing directory contents
    940      * @throws IOException if failed to create zip file
    941      * @deprecated Moved to {@link ZipUtil#createZip(File)}.
    942      */
    943     @Deprecated
    944     public static File createZip(File dir) throws IOException {
    945         return ZipUtil.createZip(dir);
    946     }
    947 
    948     /**
    949      * Utility method to create a zip file containing the given directory and
    950      * all its contents.
    951      *
    952      * @param dir the directory to zip
    953      * @param zipFile the zip file to create - it should not already exist
    954      * @throws IOException if failed to create zip file
    955      * @deprecated Moved to {@link ZipUtil#createZip(File, File)}.
    956      */
    957     @Deprecated
    958     public static void createZip(File dir, File zipFile) throws IOException {
    959         ZipUtil.createZip(dir, zipFile);
    960     }
    961 
    962     /**
    963      * Close an open {@link ZipFile}, ignoring any exceptions.
    964      *
    965      * @param zipFile the file to close
    966      * @deprecated Moved to {@link ZipUtil#closeZip(ZipFile)}.
    967      */
    968     @Deprecated
    969     public static void closeZip(ZipFile zipFile) {
    970         ZipUtil.closeZip(zipFile);
    971     }
    972 
    973     /**
    974      * Helper method to create a gzipped version of a single file.
    975      *
    976      * @param file     the original file
    977      * @param gzipFile the file to place compressed contents in
    978      * @throws IOException
    979      * @deprecated Moved to {@link ZipUtil#gzipFile(File, File)}.
    980      */
    981     @Deprecated
    982     public static void gzipFile(File file, File gzipFile) throws IOException {
    983         ZipUtil.gzipFile(file, gzipFile);
    984     }
    985 
    986     /**
    987      * Helper method to calculate md5 for a file.
    988      *
    989      * @param file
    990      * @return md5 of the file
    991      * @throws IOException
    992      */
    993     public static String calculateMd5(File file) throws IOException {
    994         FileInputStream inputSource = new FileInputStream(file);
    995         return StreamUtil.calculateMd5(inputSource);
    996     }
    997 
    998     /**
    999      * Converts an integer representing unix mode to a set of {@link PosixFilePermission}s
   1000      */
   1001     public static Set<PosixFilePermission> unixModeToPosix(int mode) {
   1002         Set<PosixFilePermission> result = EnumSet.noneOf(PosixFilePermission.class);
   1003         for (PosixFilePermission pfp : EnumSet.allOf(PosixFilePermission.class)) {
   1004             int m = PERM_MODE_MAP.get(pfp);
   1005             if ((m & mode) == m) {
   1006                 result.add(pfp);
   1007             }
   1008         }
   1009         return result;
   1010     }
   1011 
   1012     /**
   1013      * Get all file paths of files in the given directory with name matching the given filter
   1014      *
   1015      * @param dir {@link File} object of the directory to search for files recursively
   1016      * @param filter {@link String} of the regex to match file names
   1017      * @return a set of {@link String} of the file paths
   1018      */
   1019     public static Set<String> findFiles(File dir, String filter) throws IOException {
   1020         Set<String> files = new HashSet<>();
   1021         Files.walk(Paths.get(dir.getAbsolutePath()), FileVisitOption.FOLLOW_LINKS)
   1022                 .filter(path -> path.getFileName().toString().matches(filter))
   1023                 .forEach(path -> files.add(path.toString()));
   1024         return files;
   1025     }
   1026 
   1027     /**
   1028      * Get all file paths of files in the given directory with name matching the given filter
   1029      *
   1030      * @param dir {@link File} object of the directory to search for files recursively
   1031      * @param filter {@link String} of the regex to match file names
   1032      * @return a set of {@link File} of the file objects. @See {@link #findFiles(File, String)}
   1033      */
   1034     public static Set<File> findFilesObject(File dir, String filter) throws IOException {
   1035         Set<File> files = new HashSet<>();
   1036         Files.walk(Paths.get(dir.getAbsolutePath()), FileVisitOption.FOLLOW_LINKS)
   1037                 .filter(path -> path.getFileName().toString().matches(filter))
   1038                 .forEach(path -> files.add(path.toFile()));
   1039         return files;
   1040     }
   1041 
   1042     /**
   1043      * Get file's content type based it's extension.
   1044      * @param filePath the file path
   1045      * @return content type
   1046      */
   1047     public static String getContentType(String filePath) {
   1048         int index = filePath.lastIndexOf('.');
   1049         String ext = "";
   1050         if (index >= 0) {
   1051             ext = filePath.substring(index + 1);
   1052         }
   1053         LogDataType[] dataTypes = LogDataType.values();
   1054         for (LogDataType dataType: dataTypes) {
   1055             if (ext.equals(dataType.getFileExt())) {
   1056                 return dataType.getContentType();
   1057             }
   1058         }
   1059         return LogDataType.UNKNOWN.getContentType();
   1060     }
   1061 
   1062     /**
   1063      * Save a resource file to a directory.
   1064      *
   1065      * @param resourceStream a {link InputStream} object to the resource to be saved.
   1066      * @param destDir a {@link File} object of a directory to where the resource file will be saved.
   1067      * @param targetFileName a {@link String} for the name of the file to be saved to.
   1068      * @return a {@link File} object of the file saved.
   1069      * @throws IOException if the file failed to be saved.
   1070      */
   1071     public static File saveResourceFile(
   1072             InputStream resourceStream, File destDir, String targetFileName) throws IOException {
   1073         FileWriter writer = null;
   1074         File file = Paths.get(destDir.getAbsolutePath(), targetFileName).toFile();
   1075         try {
   1076             writer = new FileWriter(file);
   1077             StreamUtil.copyStreamToWriter(resourceStream, writer);
   1078             return file;
   1079         } catch (IOException e) {
   1080             CLog.e("IOException while saving resource %s/%s", destDir, targetFileName);
   1081             deleteFile(file);
   1082             throw e;
   1083         } finally {
   1084             if (writer != null) {
   1085                 writer.close();
   1086             }
   1087             if (resourceStream != null) {
   1088                 resourceStream.close();
   1089             }
   1090         }
   1091     }
   1092 }
   1093