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