• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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