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