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