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