FileUtils.java revision 41be35dd011dbd7ed3a9d08e8fe3eb85ea0e5b64
1/* 2 * Copyright (C) 2006 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 17package android.os; 18 19import android.provider.DocumentsContract.Document; 20import android.system.ErrnoException; 21import android.system.Os; 22import android.text.TextUtils; 23import android.util.Log; 24import android.util.Slog; 25import android.webkit.MimeTypeMap; 26 27import java.io.BufferedInputStream; 28import java.io.ByteArrayOutputStream; 29import java.io.File; 30import java.io.FileDescriptor; 31import java.io.FileInputStream; 32import java.io.FileNotFoundException; 33import java.io.FileOutputStream; 34import java.io.FileWriter; 35import java.io.IOException; 36import java.io.InputStream; 37import java.util.Arrays; 38import java.util.Comparator; 39import java.util.Objects; 40import java.util.regex.Pattern; 41import java.util.zip.CRC32; 42import java.util.zip.CheckedInputStream; 43 44/** 45 * Tools for managing files. Not for public consumption. 46 * @hide 47 */ 48public class FileUtils { 49 private static final String TAG = "FileUtils"; 50 51 public static final int S_IRWXU = 00700; 52 public static final int S_IRUSR = 00400; 53 public static final int S_IWUSR = 00200; 54 public static final int S_IXUSR = 00100; 55 56 public static final int S_IRWXG = 00070; 57 public static final int S_IRGRP = 00040; 58 public static final int S_IWGRP = 00020; 59 public static final int S_IXGRP = 00010; 60 61 public static final int S_IRWXO = 00007; 62 public static final int S_IROTH = 00004; 63 public static final int S_IWOTH = 00002; 64 public static final int S_IXOTH = 00001; 65 66 /** Regular expression for safe filenames: no spaces or metacharacters */ 67 private static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+"); 68 69 /** 70 * Set owner and mode of of given {@link File}. 71 * 72 * @param mode to apply through {@code chmod} 73 * @param uid to apply through {@code chown}, or -1 to leave unchanged 74 * @param gid to apply through {@code chown}, or -1 to leave unchanged 75 * @return 0 on success, otherwise errno. 76 */ 77 public static int setPermissions(File path, int mode, int uid, int gid) { 78 return setPermissions(path.getAbsolutePath(), mode, uid, gid); 79 } 80 81 /** 82 * Set owner and mode of of given path. 83 * 84 * @param mode to apply through {@code chmod} 85 * @param uid to apply through {@code chown}, or -1 to leave unchanged 86 * @param gid to apply through {@code chown}, or -1 to leave unchanged 87 * @return 0 on success, otherwise errno. 88 */ 89 public static int setPermissions(String path, int mode, int uid, int gid) { 90 try { 91 Os.chmod(path, mode); 92 } catch (ErrnoException e) { 93 Slog.w(TAG, "Failed to chmod(" + path + "): " + e); 94 return e.errno; 95 } 96 97 if (uid >= 0 || gid >= 0) { 98 try { 99 Os.chown(path, uid, gid); 100 } catch (ErrnoException e) { 101 Slog.w(TAG, "Failed to chown(" + path + "): " + e); 102 return e.errno; 103 } 104 } 105 106 return 0; 107 } 108 109 /** 110 * Set owner and mode of of given {@link FileDescriptor}. 111 * 112 * @param mode to apply through {@code chmod} 113 * @param uid to apply through {@code chown}, or -1 to leave unchanged 114 * @param gid to apply through {@code chown}, or -1 to leave unchanged 115 * @return 0 on success, otherwise errno. 116 */ 117 public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) { 118 try { 119 Os.fchmod(fd, mode); 120 } catch (ErrnoException e) { 121 Slog.w(TAG, "Failed to fchmod(): " + e); 122 return e.errno; 123 } 124 125 if (uid >= 0 || gid >= 0) { 126 try { 127 Os.fchown(fd, uid, gid); 128 } catch (ErrnoException e) { 129 Slog.w(TAG, "Failed to fchown(): " + e); 130 return e.errno; 131 } 132 } 133 134 return 0; 135 } 136 137 /** 138 * Return owning UID of given path, otherwise -1. 139 */ 140 public static int getUid(String path) { 141 try { 142 return Os.stat(path).st_uid; 143 } catch (ErrnoException e) { 144 return -1; 145 } 146 } 147 148 /** 149 * Perform an fsync on the given FileOutputStream. The stream at this 150 * point must be flushed but not yet closed. 151 */ 152 public static boolean sync(FileOutputStream stream) { 153 try { 154 if (stream != null) { 155 stream.getFD().sync(); 156 } 157 return true; 158 } catch (IOException e) { 159 } 160 return false; 161 } 162 163 // copy a file from srcFile to destFile, return true if succeed, return 164 // false if fail 165 public static boolean copyFile(File srcFile, File destFile) { 166 boolean result = false; 167 try { 168 InputStream in = new FileInputStream(srcFile); 169 try { 170 result = copyToFile(in, destFile); 171 } finally { 172 in.close(); 173 } 174 } catch (IOException e) { 175 result = false; 176 } 177 return result; 178 } 179 180 /** 181 * Copy data from a source stream to destFile. 182 * Return true if succeed, return false if failed. 183 */ 184 public static boolean copyToFile(InputStream inputStream, File destFile) { 185 try { 186 if (destFile.exists()) { 187 destFile.delete(); 188 } 189 FileOutputStream out = new FileOutputStream(destFile); 190 try { 191 byte[] buffer = new byte[4096]; 192 int bytesRead; 193 while ((bytesRead = inputStream.read(buffer)) >= 0) { 194 out.write(buffer, 0, bytesRead); 195 } 196 } finally { 197 out.flush(); 198 try { 199 out.getFD().sync(); 200 } catch (IOException e) { 201 } 202 out.close(); 203 } 204 return true; 205 } catch (IOException e) { 206 return false; 207 } 208 } 209 210 /** 211 * Check if a filename is "safe" (no metacharacters or spaces). 212 * @param file The file to check 213 */ 214 public static boolean isFilenameSafe(File file) { 215 // Note, we check whether it matches what's known to be safe, 216 // rather than what's known to be unsafe. Non-ASCII, control 217 // characters, etc. are all unsafe by default. 218 return SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches(); 219 } 220 221 /** 222 * Read a text file into a String, optionally limiting the length. 223 * @param file to read (will not seek, so things like /proc files are OK) 224 * @param max length (positive for head, negative of tail, 0 for no limit) 225 * @param ellipsis to add of the file was truncated (can be null) 226 * @return the contents of the file, possibly truncated 227 * @throws IOException if something goes wrong reading the file 228 */ 229 public static String readTextFile(File file, int max, String ellipsis) throws IOException { 230 InputStream input = new FileInputStream(file); 231 // wrapping a BufferedInputStream around it because when reading /proc with unbuffered 232 // input stream, bytes read not equal to buffer size is not necessarily the correct 233 // indication for EOF; but it is true for BufferedInputStream due to its implementation. 234 BufferedInputStream bis = new BufferedInputStream(input); 235 try { 236 long size = file.length(); 237 if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes 238 if (size > 0 && (max == 0 || size < max)) max = (int) size; 239 byte[] data = new byte[max + 1]; 240 int length = bis.read(data); 241 if (length <= 0) return ""; 242 if (length <= max) return new String(data, 0, length); 243 if (ellipsis == null) return new String(data, 0, max); 244 return new String(data, 0, max) + ellipsis; 245 } else if (max < 0) { // "tail" mode: keep the last N 246 int len; 247 boolean rolled = false; 248 byte[] last = null; 249 byte[] data = null; 250 do { 251 if (last != null) rolled = true; 252 byte[] tmp = last; last = data; data = tmp; 253 if (data == null) data = new byte[-max]; 254 len = bis.read(data); 255 } while (len == data.length); 256 257 if (last == null && len <= 0) return ""; 258 if (last == null) return new String(data, 0, len); 259 if (len > 0) { 260 rolled = true; 261 System.arraycopy(last, len, last, 0, last.length - len); 262 System.arraycopy(data, 0, last, last.length - len, len); 263 } 264 if (ellipsis == null || !rolled) return new String(last); 265 return ellipsis + new String(last); 266 } else { // "cat" mode: size unknown, read it all in streaming fashion 267 ByteArrayOutputStream contents = new ByteArrayOutputStream(); 268 int len; 269 byte[] data = new byte[1024]; 270 do { 271 len = bis.read(data); 272 if (len > 0) contents.write(data, 0, len); 273 } while (len == data.length); 274 return contents.toString(); 275 } 276 } finally { 277 bis.close(); 278 input.close(); 279 } 280 } 281 282 /** 283 * Writes string to file. Basically same as "echo -n $string > $filename" 284 * 285 * @param filename 286 * @param string 287 * @throws IOException 288 */ 289 public static void stringToFile(String filename, String string) throws IOException { 290 FileWriter out = new FileWriter(filename); 291 try { 292 out.write(string); 293 } finally { 294 out.close(); 295 } 296 } 297 298 /** 299 * Computes the checksum of a file using the CRC32 checksum routine. 300 * The value of the checksum is returned. 301 * 302 * @param file the file to checksum, must not be null 303 * @return the checksum value or an exception is thrown. 304 */ 305 public static long checksumCrc32(File file) throws FileNotFoundException, IOException { 306 CRC32 checkSummer = new CRC32(); 307 CheckedInputStream cis = null; 308 309 try { 310 cis = new CheckedInputStream( new FileInputStream(file), checkSummer); 311 byte[] buf = new byte[128]; 312 while(cis.read(buf) >= 0) { 313 // Just read for checksum to get calculated. 314 } 315 return checkSummer.getValue(); 316 } finally { 317 if (cis != null) { 318 try { 319 cis.close(); 320 } catch (IOException e) { 321 } 322 } 323 } 324 } 325 326 /** 327 * Delete older files in a directory until only those matching the given 328 * constraints remain. 329 * 330 * @param minCount Always keep at least this many files. 331 * @param minAge Always keep files younger than this age. 332 * @return if any files were deleted. 333 */ 334 public static boolean deleteOlderFiles(File dir, int minCount, long minAge) { 335 if (minCount < 0 || minAge < 0) { 336 throw new IllegalArgumentException("Constraints must be positive or 0"); 337 } 338 339 final File[] files = dir.listFiles(); 340 if (files == null) return false; 341 342 // Sort with newest files first 343 Arrays.sort(files, new Comparator<File>() { 344 @Override 345 public int compare(File lhs, File rhs) { 346 return (int) (rhs.lastModified() - lhs.lastModified()); 347 } 348 }); 349 350 // Keep at least minCount files 351 boolean deleted = false; 352 for (int i = minCount; i < files.length; i++) { 353 final File file = files[i]; 354 355 // Keep files newer than minAge 356 final long age = System.currentTimeMillis() - file.lastModified(); 357 if (age > minAge) { 358 if (file.delete()) { 359 Log.d(TAG, "Deleted old file " + file); 360 deleted = true; 361 } 362 } 363 } 364 return deleted; 365 } 366 367 /** 368 * Test if a file lives under the given directory, either as a direct child 369 * or a distant grandchild. 370 * <p> 371 * Both files <em>must</em> have been resolved using 372 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 373 * attacks. 374 */ 375 public static boolean contains(File[] dirs, File file) { 376 for (File dir : dirs) { 377 if (contains(dir, file)) { 378 return true; 379 } 380 } 381 return false; 382 } 383 384 /** 385 * Test if a file lives under the given directory, either as a direct child 386 * or a distant grandchild. 387 * <p> 388 * Both files <em>must</em> have been resolved using 389 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 390 * attacks. 391 */ 392 public static boolean contains(File dir, File file) { 393 if (dir == null || file == null) return false; 394 395 String dirPath = dir.getAbsolutePath(); 396 String filePath = file.getAbsolutePath(); 397 398 if (dirPath.equals(filePath)) { 399 return true; 400 } 401 402 if (!dirPath.endsWith("/")) { 403 dirPath += "/"; 404 } 405 return filePath.startsWith(dirPath); 406 } 407 408 public static boolean deleteContents(File dir) { 409 File[] files = dir.listFiles(); 410 boolean success = true; 411 if (files != null) { 412 for (File file : files) { 413 if (file.isDirectory()) { 414 success &= deleteContents(file); 415 } 416 if (!file.delete()) { 417 Log.w(TAG, "Failed to delete " + file); 418 success = false; 419 } 420 } 421 } 422 return success; 423 } 424 425 private static boolean isValidExtFilenameChar(char c) { 426 switch (c) { 427 case '\0': 428 case '/': 429 return false; 430 default: 431 return true; 432 } 433 } 434 435 /** 436 * Check if given filename is valid for an ext4 filesystem. 437 */ 438 public static boolean isValidExtFilename(String name) { 439 return (name != null) && name.equals(buildValidExtFilename(name)); 440 } 441 442 /** 443 * Mutate the given filename to make it valid for an ext4 filesystem, 444 * replacing any invalid characters with "_". 445 */ 446 public static String buildValidExtFilename(String name) { 447 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 448 return "(invalid)"; 449 } 450 final StringBuilder res = new StringBuilder(name.length()); 451 for (int i = 0; i < name.length(); i++) { 452 final char c = name.charAt(i); 453 if (isValidExtFilenameChar(c)) { 454 res.append(c); 455 } else { 456 res.append('_'); 457 } 458 } 459 return res.toString(); 460 } 461 462 private static boolean isValidFatFilenameChar(char c) { 463 if ((0x00 <= c && c <= 0x1f)) { 464 return false; 465 } 466 switch (c) { 467 case '"': 468 case '*': 469 case '/': 470 case ':': 471 case '<': 472 case '>': 473 case '?': 474 case '\\': 475 case '|': 476 case 0x7F: 477 return false; 478 default: 479 return true; 480 } 481 } 482 483 /** 484 * Check if given filename is valid for a FAT filesystem. 485 */ 486 public static boolean isValidFatFilename(String name) { 487 return (name != null) && name.equals(buildValidFatFilename(name)); 488 } 489 490 /** 491 * Mutate the given filename to make it valid for a FAT filesystem, 492 * replacing any invalid characters with "_". 493 */ 494 public static String buildValidFatFilename(String name) { 495 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 496 return "(invalid)"; 497 } 498 final StringBuilder res = new StringBuilder(name.length()); 499 for (int i = 0; i < name.length(); i++) { 500 final char c = name.charAt(i); 501 if (isValidFatFilenameChar(c)) { 502 res.append(c); 503 } else { 504 res.append('_'); 505 } 506 } 507 return res.toString(); 508 } 509 510 public static String rewriteAfterRename(File beforeDir, File afterDir, String path) { 511 if (path == null) return null; 512 final File result = rewriteAfterRename(beforeDir, afterDir, new File(path)); 513 return (result != null) ? result.getAbsolutePath() : null; 514 } 515 516 public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) { 517 if (paths == null) return null; 518 final String[] result = new String[paths.length]; 519 for (int i = 0; i < paths.length; i++) { 520 result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]); 521 } 522 return result; 523 } 524 525 /** 526 * Given a path under the "before" directory, rewrite it to live under the 527 * "after" directory. For example, {@code /before/foo/bar.txt} would become 528 * {@code /after/foo/bar.txt}. 529 */ 530 public static File rewriteAfterRename(File beforeDir, File afterDir, File file) { 531 if (file == null || beforeDir == null || afterDir == null) return null; 532 if (contains(beforeDir, file)) { 533 final String splice = file.getAbsolutePath().substring( 534 beforeDir.getAbsolutePath().length()); 535 return new File(afterDir, splice); 536 } 537 return null; 538 } 539 540 /** 541 * Generates a unique file name under the given parent directory. If the display name doesn't 542 * have an extension that matches the requested MIME type, the default extension for that MIME 543 * type is appended. If a file already exists, the name is appended with a numerical value to 544 * make it unique. 545 * 546 * For example, the display name 'example' with 'text/plain' MIME might produce 547 * 'example.txt' or 'example (1).txt', etc. 548 * 549 * @throws FileNotFoundException 550 */ 551 public static File buildUniqueFile(File parent, String mimeType, String displayName) 552 throws FileNotFoundException { 553 String name; 554 String ext; 555 556 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 557 name = displayName; 558 ext = null; 559 } else { 560 String mimeTypeFromExt; 561 562 // Extract requested extension from display name 563 final int lastDot = displayName.lastIndexOf('.'); 564 if (lastDot >= 0) { 565 name = displayName.substring(0, lastDot); 566 ext = displayName.substring(lastDot + 1); 567 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 568 ext.toLowerCase()); 569 } else { 570 name = displayName; 571 ext = null; 572 mimeTypeFromExt = null; 573 } 574 575 if (mimeTypeFromExt == null) { 576 mimeTypeFromExt = "application/octet-stream"; 577 } 578 579 final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType( 580 mimeType); 581 if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) { 582 // Extension maps back to requested MIME type; allow it 583 } else { 584 // No match; insist that create file matches requested MIME 585 name = displayName; 586 ext = extFromMimeType; 587 } 588 } 589 590 File file = buildFile(parent, name, ext); 591 592 // If conflicting file, try adding counter suffix 593 int n = 0; 594 while (file.exists()) { 595 if (n++ >= 32) { 596 throw new FileNotFoundException("Failed to create unique file"); 597 } 598 file = buildFile(parent, name + " (" + n + ")", ext); 599 } 600 601 return file; 602 } 603 604 private static File buildFile(File parent, String name, String ext) { 605 if (TextUtils.isEmpty(ext)) { 606 return new File(parent, name); 607 } else { 608 return new File(parent, name + "." + ext); 609 } 610 } 611} 612