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