1 /* 2 * Copyright 2022 Google LLC 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.google.android.libraries.mobiledatadownload.file.openers; 17 18 import android.net.Uri; 19 import android.os.Build.VERSION; 20 import android.os.Build.VERSION_CODES; 21 import android.system.Os; 22 import android.system.OsConstants; 23 import android.system.StructStat; 24 import com.google.android.libraries.mobiledatadownload.file.OpenContext; 25 import com.google.android.libraries.mobiledatadownload.file.Opener; 26 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 27 import com.google.android.libraries.mobiledatadownload.file.common.internal.Exceptions; 28 import com.google.errorprone.annotations.CanIgnoreReturnValue; 29 import java.io.File; 30 import java.io.IOException; 31 import java.util.ArrayList; 32 import java.util.List; 33 34 /** 35 * Deletes the file or directory at the given URI recursively. This behaves similarly to {@link 36 * SynchronousFileStorage#deleteRecursively} except as described in the following paragraph. 37 * 38 * <p>If an IO exception occurs attempting to read, open, or delete any file under the given 39 * directory, this method skips that file and continues. All such exceptions are collected and, 40 * after attempting to delete all files, an {@code IOException} is thrown containing those 41 * exceptions as {@linkplain Throwable#getSuppressed() suppressed exceptions}. 42 * 43 * <p>WARNING: this opener suffers from the following caveats and should be used with caution: 44 * 45 * <ul> 46 * <li>Directory tree traversal is not an atomic operation 47 * </ul> 48 * 49 * <p>Usage: <code> 50 * storage.open(uri, RecursiveDeleteOpener.create()); 51 * </code> 52 */ 53 public final class RecursiveDeleteOpener implements Opener<Void> { 54 private boolean noFollowLinks; 55 create()56 public static RecursiveDeleteOpener create() { 57 return new RecursiveDeleteOpener(); 58 } 59 60 @CanIgnoreReturnValue withNoFollowLinks()61 public RecursiveDeleteOpener withNoFollowLinks() { 62 this.noFollowLinks = true; 63 return this; 64 } 65 66 @Override open(OpenContext openContext)67 public Void open(OpenContext openContext) throws IOException { 68 List<IOException> exceptions = new ArrayList<>(); 69 deleteRecursively(openContext.storage(), openContext.encodedUri(), exceptions); 70 if (!exceptions.isEmpty()) { 71 throw Exceptions.combinedIOException("Failed to delete one or more files", exceptions); 72 } 73 74 return null; // for Void return type 75 } 76 deleteRecursively( SynchronousFileStorage storage, Uri uri, List<IOException> exceptions)77 private void deleteRecursively( 78 SynchronousFileStorage storage, Uri uri, List<IOException> exceptions) { 79 ReadFileOpener readFileOpener = ReadFileOpener.create().withShortCircuit(); 80 try { 81 if (storage.isDirectory(uri)) { 82 if (!noFollowLinks || !isSymlink(uri, storage, readFileOpener)) { 83 for (Uri child : storage.children(uri)) { 84 deleteRecursively(storage, child, exceptions); 85 } 86 } 87 storage.deleteDirectory(uri); 88 } else { 89 storage.deleteFile(uri); 90 } 91 } catch (IOException e) { 92 exceptions.add(e); 93 } 94 } 95 isSymlink( Uri uri, SynchronousFileStorage storage, ReadFileOpener readFileOpener)96 private static boolean isSymlink( 97 Uri uri, SynchronousFileStorage storage, ReadFileOpener readFileOpener) { 98 if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 99 try { 100 File file = storage.open(uri, readFileOpener); 101 if (file == null || !file.exists()) { 102 return false; 103 } 104 StructStat stat = Os.lstat(file.getAbsolutePath()); 105 return (stat.st_mode & OsConstants.S_IFLNK) != 0; 106 } catch (Exception e) { 107 // NOTE: this should be ErrnoException, but we're forced to catch Exception to avoid 108 // breaking lower sdk levels (exceptions aren't stripped from dead code blocks). 109 return false; 110 } 111 } 112 return false; 113 } 114 } 115