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