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         // We expect a status fail because 'chmod' requires arguments.
247         String stderr = result.getStderr();
248         if (CommandStatus.FAILED.equals(result.getStatus()) &&
249                 (stderr.contains("chmod: missing operand") || stderr.contains("usage: "))) {
250             return true;
251         }
252         CLog.w("Chmod is not supported by this OS.");
253         return false;
254     }
255 
256     /**
257      * Recursively set read and exec (if folder) permissions for given file.
258      */
setReadableRecursive(File file)259     public static void setReadableRecursive(File file) {
260         file.setReadable(true);
261         if (file.isDirectory()) {
262             file.setExecutable(true);
263             File[] children = file.listFiles();
264             if (children != null) {
265                 for (File childFile : file.listFiles()) {
266                     setReadableRecursive(childFile);
267                 }
268             }
269         }
270     }
271 
272     /**
273      * Helper function to create a temp directory in the system default temporary file directory.
274      *
275      * @param prefix The prefix string to be used in generating the file's name; must be at least
276      *            three characters long
277      * @return the created directory
278      * @throws IOException if file could not be created
279      */
createTempDir(String prefix)280     public static File createTempDir(String prefix) throws IOException {
281         return createTempDir(prefix, null);
282     }
283 
284     /**
285      * Helper function to create a temp directory.
286      *
287      * @param prefix The prefix string to be used in generating the file's name; must be at least
288      *            three characters long
289      * @param parentDir The parent directory in which the directory is to be created. If
290      *            <code>null</code> the system default temp directory will be used.
291      * @return the created directory
292      * @throws IOException if file could not be created
293      */
createTempDir(String prefix, File parentDir)294     public static File createTempDir(String prefix, File parentDir) throws IOException {
295         // create a temp file with unique name, then make it a directory
296         if (parentDir != null) {
297             CLog.d("Creating temp directory at %s with prefix \"%s\"",
298               parentDir.getAbsolutePath(), prefix);
299         }
300         File tmpDir = File.createTempFile(prefix, "", parentDir);
301         return deleteFileAndCreateDirWithSameName(tmpDir);
302     }
303 
deleteFileAndCreateDirWithSameName(File tmpDir)304     private static File deleteFileAndCreateDirWithSameName(File tmpDir) throws IOException {
305         tmpDir.delete();
306         return createDir(tmpDir);
307     }
308 
createDir(File tmpDir)309     private static File createDir(File tmpDir) throws IOException {
310         if (!tmpDir.mkdirs()) {
311             throw new IOException("unable to create directory");
312         }
313         return tmpDir;
314     }
315 
316     /**
317      * Helper function to create a named directory inside your temp folder.
318      * <p/>
319      * This directory will not have it's name randomized. If the directory already exists it will
320      * be returned.
321      *
322      * @param name The name of the directory to create in your tmp folder.
323      * @return the created directory
324      */
createNamedTempDir(String name)325     public static File createNamedTempDir(String name) throws IOException {
326         File namedTmpDir = new File(System.getProperty("java.io.tmpdir"), name);
327         if (!namedTmpDir.exists()) {
328             createDir(namedTmpDir);
329         }
330         return namedTmpDir;
331     }
332 
333     /**
334      * Helper wrapper function around {@link File#createTempFile(String, String)} that audits for
335      * potential out of disk space scenario.
336      *
337      * @see File#createTempFile(String, String)
338      * @throws LowDiskSpaceException if disk space on temporary partition is lower than minimum
339      *             allowed
340      */
createTempFile(String prefix, String suffix)341     public static File createTempFile(String prefix, String suffix) throws IOException {
342         return internalCreateTempFile(prefix, suffix, null);
343     }
344 
345     /**
346      * Helper wrapper function around {@link File#createTempFile(String, String, File)}
347      * that audits for potential out of disk space scenario.
348      *
349      * @see File#createTempFile(String, String, File)
350      * @throws LowDiskSpaceException if disk space on partition is lower than minimum allowed
351      */
createTempFile(String prefix, String suffix, File parentDir)352     public static File createTempFile(String prefix, String suffix, File parentDir)
353             throws IOException {
354         return internalCreateTempFile(prefix, suffix, parentDir);
355     }
356 
357     /**
358      * Internal helper to create a temporary file.
359      */
internalCreateTempFile(String prefix, String suffix, File parentDir)360     private static File internalCreateTempFile(String prefix, String suffix, File parentDir)
361             throws IOException {
362         // File.createTempFile add an additional random long in the name so we remove the length.
363         int overflowLength = prefix.length() + 19 - FILESYSTEM_FILENAME_MAX_LENGTH;
364         if (suffix != null) {
365             // suffix may be null
366             overflowLength += suffix.length();
367         }
368         if (overflowLength > 0) {
369             CLog.w("Filename for prefix: %s and suffix: %s, would be too long for FileSystem,"
370                     + "truncating it.", prefix, suffix);
371             // We truncate from suffix in priority because File.createTempFile wants prefix to be
372             // at least 3 characters.
373             if (suffix.length() >= overflowLength) {
374                 int temp = overflowLength;
375                 overflowLength -= suffix.length();
376                 suffix = suffix.substring(temp, suffix.length());
377             } else {
378                 overflowLength -= suffix.length();
379                 suffix = "";
380             }
381             if (overflowLength > 0) {
382                 // Whatever remaining to remove after suffix has been truncating should be inside
383                 // prefix, otherwise there would not be overflow.
384                 prefix = prefix.substring(0, prefix.length() - overflowLength);
385             }
386         }
387         File returnFile = null;
388         if (parentDir != null) {
389             CLog.d("Creating temp file at %s with prefix \"%s\" suffix \"%s\"",
390                     parentDir.getAbsolutePath(), prefix, suffix);
391         }
392         returnFile = File.createTempFile(prefix, suffix, parentDir);
393         verifyDiskSpace(returnFile);
394         return returnFile;
395     }
396 
397     /**
398      * A helper method that hardlinks a file to another file. Fallback to copy in case of cross
399      * partition linking.
400      *
401      * @param origFile the original file
402      * @param destFile the destination file
403      * @throws IOException if failed to hardlink file
404      */
hardlinkFile(File origFile, File destFile)405     public static void hardlinkFile(File origFile, File destFile) throws IOException {
406         hardlinkFile(origFile, destFile, false);
407     }
408 
409     /**
410      * A helper method that hardlinks a file to another file. Fallback to copy in case of cross
411      * partition linking.
412      *
413      * @param origFile the original file
414      * @param destFile the destination file
415      * @param ignoreExistingFile If True and the file being linked already exists, skip the
416      *     exception.
417      * @throws IOException if failed to hardlink file
418      */
hardlinkFile(File origFile, File destFile, boolean ignoreExistingFile)419     public static void hardlinkFile(File origFile, File destFile, boolean ignoreExistingFile)
420             throws IOException {
421         try {
422             Files.createLink(destFile.toPath(), origFile.toPath());
423         } catch (FileAlreadyExistsException e) {
424             if (!ignoreExistingFile) {
425                 throw e;
426             }
427         } catch (FileSystemException e) {
428             if (e.getMessage().contains("Invalid cross-device link")) {
429                 CLog.d("Hardlink failed: '%s', falling back to copy.", e.getMessage());
430                 copyFile(origFile, destFile);
431                 return;
432             }
433             throw e;
434         }
435     }
436 
437     /**
438      * A helper method that symlinks a file to another file
439      *
440      * @param origFile the original file
441      * @param destFile the destination file
442      * @throws IOException if failed to symlink file
443      */
symlinkFile(File origFile, File destFile)444     public static void symlinkFile(File origFile, File destFile) throws IOException {
445         CLog.d(
446                 "Attempting symlink from %s to %s",
447                 origFile.getAbsolutePath(), destFile.getAbsolutePath());
448         Files.createSymbolicLink(destFile.toPath(), origFile.toPath());
449     }
450 
451     /**
452      * Recursively hardlink folder contents.
453      * <p/>
454      * Only supports copying of files and directories - symlinks are not copied. If the destination
455      * directory does not exist, it will be created.
456      *
457      * @param sourceDir the folder that contains the files to copy
458      * @param destDir the destination folder
459      * @throws IOException
460      */
recursiveHardlink(File sourceDir, File destDir)461     public static void recursiveHardlink(File sourceDir, File destDir) throws IOException {
462         recursiveHardlink(sourceDir, destDir, false);
463     }
464 
465     /**
466      * Recursively hardlink folder contents.
467      *
468      * <p>Only supports copying of files and directories - symlinks are not copied. If the
469      * destination directory does not exist, it will be created.
470      *
471      * @param sourceDir the folder that contains the files to copy
472      * @param destDir the destination folder
473      * @param ignoreExistingFile If True and the file being linked already exists, skip the
474      *     exception.
475      * @throws IOException
476      */
recursiveHardlink(File sourceDir, File destDir, boolean ignoreExistingFile)477     public static void recursiveHardlink(File sourceDir, File destDir, boolean ignoreExistingFile)
478             throws IOException {
479         if (!destDir.isDirectory() && !destDir.mkdir()) {
480             throw new IOException(String.format("Could not create directory %s",
481                     destDir.getAbsolutePath()));
482         }
483         for (File childFile : sourceDir.listFiles()) {
484             File destChild = new File(destDir, childFile.getName());
485             if (childFile.isDirectory()) {
486                 recursiveHardlink(childFile, destChild, ignoreExistingFile);
487             } else if (childFile.isFile()) {
488                 hardlinkFile(childFile, destChild, ignoreExistingFile);
489             }
490         }
491     }
492 
493     /**
494      * Recursively symlink folder contents.
495      *
496      * <p>Only supports copying of files and directories - symlinks are not copied. If the
497      * destination directory does not exist, it will be created.
498      *
499      * @param sourceDir the folder that contains the files to copy
500      * @param destDir the destination folder
501      * @throws IOException
502      */
recursiveSymlink(File sourceDir, File destDir)503     public static void recursiveSymlink(File sourceDir, File destDir) throws IOException {
504         if (!destDir.isDirectory() && !destDir.mkdir()) {
505             throw new IOException(
506                     String.format("Could not create directory %s", destDir.getAbsolutePath()));
507         }
508         for (File childFile : sourceDir.listFiles()) {
509             File destChild = new File(destDir, childFile.getName());
510             if (childFile.isDirectory()) {
511                 recursiveSymlink(childFile, destChild);
512             } else if (childFile.isFile()) {
513                 symlinkFile(childFile, destChild);
514             }
515         }
516     }
517 
518     /**
519      * A helper method that copies a file's contents to a local file
520      *
521      * @param origFile the original file to be copied
522      * @param destFile the destination file
523      * @throws IOException if failed to copy file
524      */
copyFile(File origFile, File destFile)525     public static void copyFile(File origFile, File destFile) throws IOException {
526         writeToFile(new FileInputStream(origFile), destFile);
527     }
528 
529     /**
530      * Recursively copy folder contents.
531      * <p/>
532      * Only supports copying of files and directories - symlinks are not copied. If the destination
533      * directory does not exist, it will be created.
534      *
535      * @param sourceDir the folder that contains the files to copy
536      * @param destDir the destination folder
537      * @throws IOException
538      */
recursiveCopy(File sourceDir, File destDir)539     public static void recursiveCopy(File sourceDir, File destDir) throws IOException {
540         File[] childFiles = sourceDir.listFiles();
541         if (childFiles == null) {
542             throw new IOException(String.format(
543                     "Failed to recursively copy. Could not determine contents for directory '%s'",
544                     sourceDir.getAbsolutePath()));
545         }
546         if (!destDir.isDirectory() && !destDir.mkdir()) {
547             throw new IOException(String.format("Could not create directory %s",
548                 destDir.getAbsolutePath()));
549         }
550         for (File childFile : childFiles) {
551             File destChild = new File(destDir, childFile.getName());
552             if (childFile.isDirectory()) {
553                 recursiveCopy(childFile, destChild);
554             } else if (childFile.isFile()) {
555                 copyFile(childFile, destChild);
556             }
557         }
558     }
559 
560     /**
561      * A helper method for reading string data from a file
562      *
563      * @param sourceFile the file to read from
564      * @throws IOException
565      * @throws FileNotFoundException
566      */
readStringFromFile(File sourceFile)567     public static String readStringFromFile(File sourceFile) throws IOException {
568         return readStringFromFile(sourceFile, 0, 0);
569     }
570 
571     /**
572      * A helper method for reading partial string data from a file
573      *
574      * @param sourceFile the file to read from
575      * @param startOffset the start offset to read from the file.
576      * @param length the number of bytes to read of the file.
577      * @throws IOException
578      * @throws FileNotFoundException
579      */
readStringFromFile(File sourceFile, long startOffset, long length)580     public static String readStringFromFile(File sourceFile, long startOffset, long length)
581             throws IOException {
582         try (FileInputStream is = new FileInputStream(sourceFile)) {
583             if (startOffset < 0) {
584                 startOffset = 0;
585             }
586             long fileLength = sourceFile.length();
587             is.skip(startOffset);
588             if (length <= 0 || fileLength <= startOffset + length) {
589                 return StreamUtil.getStringFromStream(is);
590             }
591             return StreamUtil.getStringFromStream(is, length);
592         }
593     }
594 
595     /**
596      * A helper method for writing string data to file
597      *
598      * @param inputString the input {@link String}
599      * @param destFile the destination file to write to
600      */
writeToFile(String inputString, File destFile)601     public static void writeToFile(String inputString, File destFile) throws IOException {
602         writeToFile(inputString, destFile, false);
603     }
604 
605     /**
606      * A helper method for writing or appending string data to file
607      *
608      * @param inputString the input {@link String}
609      * @param destFile the destination file to write or append to
610      * @param append append to end of file if true, overwrite otherwise
611      */
writeToFile(String inputString, File destFile, boolean append)612     public static void writeToFile(String inputString, File destFile, boolean append)
613             throws IOException {
614         writeToFile(new ByteArrayInputStream(inputString.getBytes()), destFile, append);
615     }
616 
617     /**
618      * A helper method for writing stream data to file
619      *
620      * @param input the unbuffered input stream
621      * @param destFile the destination file to write to
622      */
writeToFile(InputStream input, File destFile)623     public static void writeToFile(InputStream input, File destFile) throws IOException {
624         writeToFile(input, destFile, false);
625     }
626 
627     /**
628      * A helper method for writing stream data to file
629      *
630      * @param input the unbuffered input stream
631      * @param destFile the destination file to write or append to
632      * @param append append to end of file if true, overwrite otherwise
633      */
writeToFile( InputStream input, File destFile, boolean append)634     public static void writeToFile(
635             InputStream input, File destFile, boolean append) throws IOException {
636         // Set size to a negative value to write all content starting at the given offset.
637         writeToFile(input, destFile, append, 0, -1);
638     }
639 
640     /**
641      * A helper method for writing stream data to file
642      *
643      * @param input the unbuffered input stream
644      * @param destFile the destination file to write or append to
645      * @param append append to end of file if true, overwrite otherwise
646      * @param startOffset the start offset of the input stream to retrieve data
647      * @param size number of bytes to retrieve from the input stream, set it to a negative value to
648      *     retrieve all content starting at the given offset.
649      */
writeToFile( InputStream input, File destFile, boolean append, long startOffset, long size)650     public static void writeToFile(
651             InputStream input, File destFile, boolean append, long startOffset, long size)
652             throws IOException {
653         InputStream origStream = null;
654         OutputStream destStream = null;
655         try {
656             origStream = new BufferedInputStream(input);
657             destStream = new BufferedOutputStream(new FileOutputStream(destFile, append));
658             StreamUtil.copyStreams(origStream, destStream, startOffset, size);
659         } finally {
660             StreamUtil.close(origStream);
661             StreamUtil.flushAndCloseStream(destStream);
662         }
663     }
664 
665     /**
666      * Note: We should never use CLog in here, since it also relies on that method, this would lead
667      * to infinite recursion.
668      */
verifyDiskSpace(File file)669     private static void verifyDiskSpace(File file) {
670         // Based on empirical testing File.getUsableSpace is a low cost operation (~ 100 us for
671         // local disk, ~ 100 ms for network disk). Therefore call it every time tmp file is
672         // created
673         long usableSpace = 0L;
674         File toCheck = file;
675         if (!file.isDirectory() && file.getParentFile() != null) {
676             // If the given file is not a directory it might not work properly so using the parent
677             // in that case.
678             toCheck = file.getParentFile();
679         }
680         usableSpace = toCheck.getUsableSpace();
681 
682         long minDiskSpace = mMinDiskSpaceMb * 1024 * 1024;
683         if (usableSpace < minDiskSpace) {
684             String message =
685                     String.format(
686                             "Available space on %s is %.2f MB. Min is %d MB.",
687                             toCheck.getAbsolutePath(),
688                             usableSpace / (1024.0 * 1024.0),
689                             mMinDiskSpaceMb);
690             throw new LowDiskSpaceException(message);
691         }
692     }
693 
694     /**
695      * Recursively delete given file or directory and all its contents.
696      *
697      * @param rootDir the directory or file to be deleted; can be null
698      */
recursiveDelete(File rootDir)699     public static void recursiveDelete(File rootDir) {
700         if (rootDir != null) {
701             // We expand directories if they are not symlink
702             if (rootDir.isDirectory() && !Files.isSymbolicLink(rootDir.toPath())) {
703                 File[] childFiles = rootDir.listFiles();
704                 if (childFiles != null) {
705                     for (File child : childFiles) {
706                         recursiveDelete(child);
707                     }
708                 }
709             }
710             rootDir.delete();
711         }
712     }
713 
714     /**
715      * Gets the extension for given file name.
716      *
717      * @param fileName
718      * @return the extension or empty String if file has no extension
719      */
getExtension(String fileName)720     public static String getExtension(String fileName) {
721         int index = fileName.lastIndexOf('.');
722         if (index == -1) {
723             return "";
724         } else {
725             return fileName.substring(index);
726         }
727     }
728 
729     /**
730      * Gets the base name, without extension, of given file name.
731      * <p/>
732      * e.g. getBaseName("file.txt") will return "file"
733      *
734      * @param fileName
735      * @return the base name
736      */
getBaseName(String fileName)737     public static String getBaseName(String fileName) {
738         int index = fileName.lastIndexOf('.');
739         if (index == -1) {
740             return fileName;
741         } else {
742             return fileName.substring(0, index);
743         }
744     }
745 
746     /**
747      * Utility method to do byte-wise content comparison of two files.
748      *
749      * @return <code>true</code> if file contents are identical
750      */
compareFileContents(File file1, File file2)751     public static boolean compareFileContents(File file1, File file2) throws IOException {
752         BufferedInputStream stream1 = null;
753         BufferedInputStream stream2 = null;
754 
755         boolean result = true;
756         try {
757             stream1 = new BufferedInputStream(new FileInputStream(file1));
758             stream2 = new BufferedInputStream(new FileInputStream(file2));
759             boolean eof = false;
760             while (!eof) {
761                 int byte1 = stream1.read();
762                 int byte2 = stream2.read();
763                 if (byte1 != byte2) {
764                     result = false;
765                     break;
766                 }
767                 eof = byte1 == -1;
768             }
769         } finally {
770             StreamUtil.close(stream1);
771             StreamUtil.close(stream2);
772         }
773         return result;
774     }
775 
776     /**
777      * Helper method which constructs a unique file on temporary disk, whose name corresponds as
778      * closely as possible to the file name given by the remote file path
779      *
780      * @param remoteFilePath the '/' separated remote path to construct the name from
781      * @param parentDir the parent directory to create the file in. <code>null</code> to use the
782      * default temporary directory
783      */
createTempFileForRemote(String remoteFilePath, File parentDir)784     public static File createTempFileForRemote(String remoteFilePath, File parentDir)
785             throws IOException {
786         String[] segments = remoteFilePath.split("/");
787         // take last segment as base name
788         String remoteFileName = segments[segments.length - 1];
789         String prefix = getBaseName(remoteFileName);
790         if (prefix.length() < 3) {
791             // prefix must be at least 3 characters long
792             prefix = prefix + "XXX";
793         }
794         String fileExt = getExtension(remoteFileName);
795 
796         // create a unique file name. Add a underscore to prefix so file name is more readable
797         // e.g. myfile_57588758.img rather than myfile57588758.img
798         File tmpFile = FileUtil.createTempFile(prefix + "_", fileExt, parentDir);
799         return tmpFile;
800     }
801 
802     /**
803      * Try to delete a file. Intended for use when cleaning up
804      * in {@code finally} stanzas.
805      *
806      * @param file may be null.
807      */
deleteFile(File file)808     public static void deleteFile(File file) {
809         if (file != null) {
810             file.delete();
811         }
812     }
813 
814     /**
815      * Helper method to build a system-dependent File
816      *
817      * @param parentDir the parent directory to use.
818      * @param pathSegments the relative path segments to use
819      * @return the {@link File} representing given path, with each <var>pathSegment</var>
820      *         separated by {@link File#separatorChar}
821      */
getFileForPath(File parentDir, String... pathSegments)822     public static File getFileForPath(File parentDir, String... pathSegments) {
823         return new File(parentDir, getPath(pathSegments));
824     }
825 
826     /**
827      * Helper method to build a system-dependent relative path
828      *
829      * @param pathSegments the relative path segments to use
830      * @return the {@link String} representing given path, with each <var>pathSegment</var>
831      *         separated by {@link File#separatorChar}
832      */
getPath(String... pathSegments)833     public static String getPath(String... pathSegments) {
834         StringBuilder pathBuilder = new StringBuilder();
835         boolean isFirst = true;
836         for (String path : pathSegments) {
837             if (!isFirst) {
838                 pathBuilder.append(File.separatorChar);
839             } else {
840                 isFirst = false;
841             }
842             pathBuilder.append(path);
843         }
844         return pathBuilder.toString();
845     }
846 
847     /**
848      * Recursively search given directory for first file with given name
849      *
850      * @param dir the directory to search
851      * @param fileName the name of the file to search for
852      * @return the {@link File} or <code>null</code> if it could not be found
853      */
findFile(File dir, String fileName)854     public static File findFile(File dir, String fileName) {
855         if (dir.listFiles() != null) {
856             for (File file : dir.listFiles()) {
857                 if (file.isDirectory()) {
858                     File result = findFile(file, fileName);
859                     if (result != null) {
860                         return result;
861                     }
862                 }
863                 // after exploring the sub-dir, if the dir itself is the only match return it.
864                 if (file.getName().matches(fileName)) {
865                     return file;
866                 }
867             }
868         }
869         return null;
870     }
871 
872     /**
873      * Recursively find all directories under the given {@code rootDir}
874      *
875      * @param rootDir the root directory to search in
876      * @param relativeParent An optional parent for all {@link File}s returned. If not specified,
877      *            all {@link File}s will be relative to {@code rootDir}.
878      * @return An set of {@link File}s, representing all directories under {@code rootDir},
879      *         including {@code rootDir} itself. If {@code rootDir} is null, an empty set is
880      *         returned.
881      */
findDirsUnder(File rootDir, File relativeParent)882     public static Set<File> findDirsUnder(File rootDir, File relativeParent) {
883         Set<File> dirs = new HashSet<File>();
884         if (rootDir != null) {
885             if (!rootDir.isDirectory()) {
886                 throw new IllegalArgumentException("Can't find dirs under '" + rootDir
887                         + "'. It's not a directory.");
888             }
889             File thisDir = new File(relativeParent, rootDir.getName());
890             dirs.add(thisDir);
891             for (File file : rootDir.listFiles()) {
892                 if (file.isDirectory()) {
893                     dirs.addAll(findDirsUnder(file, thisDir));
894                 }
895             }
896         }
897         return dirs;
898     }
899 
900     /**
901      * Convert the given file size in bytes to a more readable format in X.Y[KMGT] format.
902      *
903      * @param sizeLong file size in bytes
904      * @return descriptive string of file size
905      */
convertToReadableSize(long sizeLong)906     public static String convertToReadableSize(long sizeLong) {
907 
908         double size = sizeLong;
909         for (int i = 0; i < SIZE_SPECIFIERS.length; i++) {
910             if (size < 1024) {
911                 return String.format("%.1f%c", size, SIZE_SPECIFIERS[i]);
912             }
913             size /= 1024f;
914         }
915         throw new IllegalArgumentException(
916                 String.format("Passed a file size of %.2f, I cannot count that high", size));
917     }
918 
919     /**
920      * The inverse of {@link #convertToReadableSize(long)}. Converts the readable format described
921      * in {@link #convertToReadableSize(long)} to a byte value.
922      *
923      * @param sizeString the string description of the size.
924      * @return the size in bytes
925      * @throws IllegalArgumentException if cannot recognize size
926      */
convertSizeToBytes(String sizeString)927     public static long convertSizeToBytes(String sizeString) throws IllegalArgumentException {
928         if (sizeString.isEmpty()) {
929             throw new IllegalArgumentException("invalid empty string");
930         }
931         char sizeSpecifier = sizeString.charAt(sizeString.length() - 1);
932         long multiplier = findMultiplier(sizeSpecifier);
933         try {
934             String numberString = sizeString;
935             if (multiplier != 1) {
936                 // strip off last char
937                 numberString = sizeString.substring(0, sizeString.length() - 1);
938             }
939             return multiplier * Long.parseLong(numberString);
940         } catch (NumberFormatException e) {
941             throw new IllegalArgumentException(String.format("Unrecognized size %s", sizeString));
942         }
943     }
944 
findMultiplier(char sizeSpecifier)945     private static long findMultiplier(char sizeSpecifier) {
946         long multiplier = 1;
947         for (int i = 1; i < SIZE_SPECIFIERS.length; i++) {
948             multiplier *= 1024;
949             if (sizeSpecifier == SIZE_SPECIFIERS[i]) {
950                 return multiplier;
951             }
952         }
953         // not found
954         return 1;
955     }
956 
957     /**
958      * Returns all jar files found in given directory
959      */
collectJars(File dir)960     public static List<File> collectJars(File dir) {
961         List<File> list = new ArrayList<File>();
962         File[] jarFiles = dir.listFiles(new JarFilter());
963         if (jarFiles != null) {
964             list.addAll(Arrays.asList(dir.listFiles(new JarFilter())));
965         }
966         return list;
967     }
968 
969     private static class JarFilter implements FilenameFilter {
970         /**
971          * {@inheritDoc}
972          */
973         @Override
accept(File dir, String name)974         public boolean accept(File dir, String name) {
975             return name.endsWith(".jar");
976         }
977     }
978 
979 
980     // Backwards-compatibility section
981     /**
982      * Utility method to extract entire contents of zip file into given directory
983      *
984      * @param zipFile the {@link ZipFile} to extract
985      * @param destDir the local dir to extract file to
986      * @throws IOException if failed to extract file
987      * @deprecated Moved to {@link ZipUtil#extractZip(ZipFile, File)}.
988      */
989     @Deprecated
extractZip(ZipFile zipFile, File destDir)990     public static void extractZip(ZipFile zipFile, File destDir) throws IOException {
991         ZipUtil.extractZip(zipFile, destDir);
992     }
993 
994     /**
995      * Utility method to extract one specific file from zip file into a tmp file
996      *
997      * @param zipFile  the {@link ZipFile} to extract
998      * @param filePath the filePath of to extract
999      * @return the {@link File} or null if not found
1000      * @throws IOException if failed to extract file
1001      * @deprecated Moved to {@link ZipUtil#extractFileFromZip(ZipFile, String)}.
1002      */
1003     @Deprecated
extractFileFromZip(ZipFile zipFile, String filePath)1004     public static File extractFileFromZip(ZipFile zipFile, String filePath) throws IOException {
1005         return ZipUtil.extractFileFromZip(zipFile, filePath);
1006     }
1007 
1008     /**
1009      * Utility method to create a temporary zip file containing the given directory and
1010      * all its contents.
1011      *
1012      * @param dir the directory to zip
1013      * @return a temporary zip {@link File} containing directory contents
1014      * @throws IOException if failed to create zip file
1015      * @deprecated Moved to {@link ZipUtil#createZip(File)}.
1016      */
1017     @Deprecated
createZip(File dir)1018     public static File createZip(File dir) throws IOException {
1019         return ZipUtil.createZip(dir);
1020     }
1021 
1022     /**
1023      * Utility method to create a zip file containing the given directory and
1024      * all its contents.
1025      *
1026      * @param dir the directory to zip
1027      * @param zipFile the zip file to create - it should not already exist
1028      * @throws IOException if failed to create zip file
1029      * @deprecated Moved to {@link ZipUtil#createZip(File, File)}.
1030      */
1031     @Deprecated
createZip(File dir, File zipFile)1032     public static void createZip(File dir, File zipFile) throws IOException {
1033         ZipUtil.createZip(dir, zipFile);
1034     }
1035 
1036     /**
1037      * Close an open {@link ZipFile}, ignoring any exceptions.
1038      *
1039      * @param zipFile the file to close
1040      * @deprecated Moved to {@link ZipUtil#closeZip(ZipFile)}.
1041      */
1042     @Deprecated
closeZip(ZipFile zipFile)1043     public static void closeZip(ZipFile zipFile) {
1044         ZipUtil.closeZip(zipFile);
1045     }
1046 
1047     /**
1048      * Helper method to create a gzipped version of a single file.
1049      *
1050      * @param file     the original file
1051      * @param gzipFile the file to place compressed contents in
1052      * @throws IOException
1053      * @deprecated Moved to {@link ZipUtil#gzipFile(File, File)}.
1054      */
1055     @Deprecated
gzipFile(File file, File gzipFile)1056     public static void gzipFile(File file, File gzipFile) throws IOException {
1057         ZipUtil.gzipFile(file, gzipFile);
1058     }
1059 
1060     /**
1061      * Helper method to calculate CRC-32 for a file.
1062      *
1063      * @param file
1064      * @return CRC-32 of the file
1065      * @throws IOException
1066      */
calculateCrc32(File file)1067     public static long calculateCrc32(File file) throws IOException {
1068         try (BufferedInputStream inputSource = new BufferedInputStream(new FileInputStream(file))) {
1069             return StreamUtil.calculateCrc32(inputSource);
1070         }
1071     }
1072 
1073     /**
1074      * Helper method to calculate md5 for a file.
1075      *
1076      * @param file
1077      * @return md5 of the file
1078      * @throws IOException
1079      */
calculateMd5(File file)1080     public static String calculateMd5(File file) throws IOException {
1081         FileInputStream inputSource = new FileInputStream(file);
1082         return StreamUtil.calculateMd5(inputSource);
1083     }
1084 
1085     /**
1086      * Helper method to calculate base64 md5 for a file.
1087      *
1088      * @param file
1089      * @return md5 of the file
1090      * @throws IOException
1091      */
calculateBase64Md5(File file)1092     public static String calculateBase64Md5(File file) throws IOException {
1093         FileInputStream inputSource = new FileInputStream(file);
1094         return StreamUtil.calculateBase64Md5(inputSource);
1095     }
1096 
1097     /**
1098      * Converts an integer representing unix mode to a set of {@link PosixFilePermission}s
1099      */
unixModeToPosix(int mode)1100     public static Set<PosixFilePermission> unixModeToPosix(int mode) {
1101         Set<PosixFilePermission> result = EnumSet.noneOf(PosixFilePermission.class);
1102         for (PosixFilePermission pfp : EnumSet.allOf(PosixFilePermission.class)) {
1103             int m = PERM_MODE_MAP.get(pfp);
1104             if ((m & mode) == m) {
1105                 result.add(pfp);
1106             }
1107         }
1108         return result;
1109     }
1110 
1111     /**
1112      * Get all file paths of files in the given directory with name matching the given filter
1113      *
1114      * @param dir {@link File} object of the directory to search for files recursively
1115      * @param filter {@link String} of the regex to match file names
1116      * @return a set of {@link String} of the file paths
1117      */
findFiles(File dir, String filter)1118     public static Set<String> findFiles(File dir, String filter) throws IOException {
1119         Set<String> files = new HashSet<>();
1120         Files.walk(Paths.get(dir.getAbsolutePath()), FileVisitOption.FOLLOW_LINKS)
1121                 .filter(path -> path.getFileName().toString().matches(filter))
1122                 .forEach(path -> files.add(path.toString()));
1123         return files;
1124     }
1125 
1126     /**
1127      * Get all file paths of files in the given directory with name matching the given filter and
1128      * also filter the found file by abi arch if abi is not null. Return the first match file found.
1129      *
1130      * @param fileName {@link String} of the regex to match file path
1131      * @param abi {@link IAbi} object of the abi to match the target
1132      * @param dirs a varargs array of {@link File} object of the directories to search for files
1133      * @return the {@link File} or <code>null</code> if it could not be found
1134      */
findFile(String fileName, IAbi abi, File... dirs)1135     public static File findFile(String fileName, IAbi abi, File... dirs) throws IOException {
1136         for (File dir : dirs) {
1137             Set<File> testSrcs = findFilesObject(dir, fileName);
1138             if (testSrcs.isEmpty()) {
1139                 continue;
1140             }
1141             Iterator<File> itr = testSrcs.iterator();
1142             if (abi == null) {
1143                 // Return the first candidate be found.
1144                 return itr.next();
1145             }
1146             while (itr.hasNext()) {
1147                 File matchFile = itr.next();
1148                 if (matchFile
1149                         .getParentFile()
1150                         .getName()
1151                         .equals(AbiUtils.getArchForAbi(abi.getName()))) {
1152                     return matchFile;
1153                 }
1154             }
1155         }
1156         // Scan dirs again without abi rule.
1157         for (File dir : dirs) {
1158             File matchFile = findFile(dir, fileName);
1159             if (matchFile != null && matchFile.exists()) {
1160                 return matchFile;
1161             }
1162         }
1163         return null;
1164     }
1165 
1166     /**
1167      * Search and return the first directory {@link File} among other directories.
1168      *
1169      * @param dirName The directory name we are looking for.
1170      * @param dirs The list of directories we are searching.
1171      * @return a {@link File} with the directory found or Null if not found.
1172      * @throws IOException
1173      */
findDirectory(String dirName, File... dirs)1174     public static File findDirectory(String dirName, File... dirs) throws IOException {
1175         for (File dir : dirs) {
1176             Set<File> testSrcs = findFilesObject(dir, dirName);
1177             if (testSrcs.isEmpty()) {
1178                 continue;
1179             }
1180             Iterator<File> itr = testSrcs.iterator();
1181             while (itr.hasNext()) {
1182                 File file = itr.next();
1183                 if (file.isDirectory()) {
1184                     return file;
1185                 }
1186             }
1187         }
1188         return null;
1189     }
1190 
1191     /**
1192      * Get all file paths of files in the given directory with name matching the given filter
1193      *
1194      * @param dir {@link File} object of the directory to search for files recursively
1195      * @param filter {@link String} of the regex to match file names
1196      * @return a set of {@link File} of the file objects. @See {@link #findFiles(File, String)}
1197      */
findFilesObject(File dir, String filter)1198     public static Set<File> findFilesObject(File dir, String filter) throws IOException {
1199         Set<File> files = new LinkedHashSet<>();
1200         Files.walk(Paths.get(dir.getAbsolutePath()), FileVisitOption.FOLLOW_LINKS)
1201                 .filter(path -> path.getFileName().toString().matches(filter))
1202                 .forEach(path -> files.add(path.toFile()));
1203         return files;
1204     }
1205 
1206     /**
1207      * Get file's content type based it's extension.
1208      * @param filePath the file path
1209      * @return content type
1210      */
getContentType(String filePath)1211     public static String getContentType(String filePath) {
1212         int index = filePath.lastIndexOf('.');
1213         String ext = "";
1214         if (index >= 0) {
1215             ext = filePath.substring(index + 1);
1216         }
1217         LogDataType[] dataTypes = LogDataType.values();
1218         for (LogDataType dataType: dataTypes) {
1219             if (ext.equals(dataType.getFileExt())) {
1220                 return dataType.getContentType();
1221             }
1222         }
1223         return LogDataType.UNKNOWN.getContentType();
1224     }
1225 
1226     /**
1227      * Save a resource file to a directory.
1228      *
1229      * @param resourceStream a {link InputStream} object to the resource to be saved.
1230      * @param destDir a {@link File} object of a directory to where the resource file will be saved.
1231      * @param targetFileName a {@link String} for the name of the file to be saved to.
1232      * @return a {@link File} object of the file saved.
1233      * @throws IOException if the file failed to be saved.
1234      */
saveResourceFile( InputStream resourceStream, File destDir, String targetFileName)1235     public static File saveResourceFile(
1236             InputStream resourceStream, File destDir, String targetFileName) throws IOException {
1237         FileWriter writer = null;
1238         File file = Paths.get(destDir.getAbsolutePath(), targetFileName).toFile();
1239         try {
1240             writer = new FileWriter(file);
1241             StreamUtil.copyStreamToWriter(resourceStream, writer);
1242             return file;
1243         } catch (IOException e) {
1244             CLog.e("IOException while saving resource %s/%s", destDir, targetFileName);
1245             deleteFile(file);
1246             throw e;
1247         } finally {
1248             if (writer != null) {
1249                 writer.close();
1250             }
1251             if (resourceStream != null) {
1252                 resourceStream.close();
1253             }
1254         }
1255     }
1256 }
1257