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