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