1 /*
2  * Copyright (C) 2018 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 
17 package com.android.internal.os;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.FileUtils;
22 import android.util.ArrayMap;
23 
24 import com.android.internal.util.Preconditions;
25 
26 import java.io.File;
27 import java.io.FileOutputStream;
28 import java.io.IOException;
29 import java.util.Arrays;
30 
31 /**
32  * Helper class for performing atomic operations on a directory, by creating a
33  * backup directory until a write has successfully completed.
34  * <p>
35  * Atomic directory guarantees directory integrity by ensuring that a directory has
36  * been completely written and sync'd to disk before removing its backup.
37  * As long as the backup directory exists, the original directory is considered
38  * to be invalid (leftover from a previous attempt to write).
39  * <p>
40  * Atomic directory does not confer any file locking semantics. Do not use this
41  * class when the directory may be accessed or modified concurrently
42  * by multiple threads or processes. The caller is responsible for ensuring
43  * appropriate mutual exclusion invariants whenever it accesses the directory.
44  * <p>
45  * To ensure atomicity you must always use this class to interact with the
46  * backing directory when checking existence, making changes, and deleting.
47  */
48 public final class AtomicDirectory {
49     private final @NonNull ArrayMap<File, FileOutputStream> mOpenFiles = new ArrayMap<>();
50     private final @NonNull File mBaseDirectory;
51     private final @NonNull File mBackupDirectory;
52 
53     private int mBaseDirectoryFd = -1;
54     private int mBackupDirectoryFd = -1;
55 
56     /**
57      * Creates a new instance.
58      *
59      * @param baseDirectory The base directory to treat atomically.
60      */
AtomicDirectory(@onNull File baseDirectory)61     public AtomicDirectory(@NonNull File baseDirectory) {
62         Preconditions.checkNotNull(baseDirectory, "baseDirectory cannot be null");
63         mBaseDirectory = baseDirectory;
64         mBackupDirectory = new File(baseDirectory.getPath() + "_bak");
65     }
66 
67     /**
68      * Gets the backup directory if present. This could be useful if you are
69      * writing new state to the dir but need to access the last persisted state
70      * at the same time. This means that this call is useful in between
71      * {@link #startWrite()} and {@link #finishWrite()} or {@link #failWrite()}.
72      * You should not modify the content returned by this method.
73      *
74      * @see #startRead()
75      */
getBackupDirectory()76     public @Nullable File getBackupDirectory() {
77         return mBackupDirectory;
78     }
79 
80     /**
81      * Starts reading this directory. After calling this method you should
82      * not make any changes to its contents.
83      *
84      * @throws IOException If an error occurs.
85      *
86      * @see #finishRead()
87      * @see #startWrite()
88      */
startRead()89     public @NonNull File startRead() throws IOException {
90         restore();
91         return getOrCreateBaseDirectory();
92     }
93 
94     /**
95      * Finishes reading this directory.
96      *
97      * @see #startRead()
98      * @see #startWrite()
99      */
finishRead()100     public void finishRead() {
101         mBaseDirectoryFd = -1;
102         mBackupDirectoryFd = -1;
103     }
104 
105     /**
106      * Starts editing this directory. After calling this method you should
107      * add content to the directory only via the APIs on this class. To open a
108      * file for writing in this directory you should use {@link #openWrite(File)}
109      * and to close the file {@link #closeWrite(FileOutputStream)}. Once all
110      * content has been written and all files closed you should commit via a
111      * call to {@link #finishWrite()} or discard via a call to {@link #failWrite()}.
112      *
113      * @throws IOException If an error occurs.
114      *
115      * @see #startRead()
116      * @see #openWrite(File)
117      * @see #finishWrite()
118      * @see #failWrite()
119      */
startWrite()120     public @NonNull File startWrite() throws IOException {
121         backup();
122         return getOrCreateBaseDirectory();
123     }
124 
125     /**
126      * Opens a file in this directory for writing.
127      *
128      * @param file The file to open. Must be a file in the base directory.
129      * @return An input stream for reading.
130      *
131      * @throws IOException If an I/O error occurs.
132      *
133      * @see #closeWrite(FileOutputStream)
134      */
openWrite(@onNull File file)135     public @NonNull FileOutputStream openWrite(@NonNull File file) throws IOException {
136         if (file.isDirectory() || !file.getParentFile().equals(getOrCreateBaseDirectory())) {
137             throw new IllegalArgumentException("Must be a file in " + getOrCreateBaseDirectory());
138         }
139         final FileOutputStream destination = new FileOutputStream(file);
140         if (mOpenFiles.put(file, destination) != null) {
141             throw new IllegalArgumentException("Already open file" + file.getCanonicalPath());
142         }
143         return destination;
144     }
145 
146     /**
147      * Closes a previously opened file.
148      *
149      * @param destination The stream to the file returned by {@link #openWrite(File)}.
150      *
151      * @see #openWrite(File)
152      */
closeWrite(@onNull FileOutputStream destination)153     public void closeWrite(@NonNull FileOutputStream destination) {
154         final int indexOfValue = mOpenFiles.indexOfValue(destination);
155         if (mOpenFiles.removeAt(indexOfValue) == null) {
156             throw new IllegalArgumentException("Unknown file stream " + destination);
157         }
158         FileUtils.sync(destination);
159         try {
160             destination.close();
161         } catch (IOException ignored) {}
162     }
163 
failWrite(@onNull FileOutputStream destination)164     public void failWrite(@NonNull FileOutputStream destination) {
165         final int indexOfValue = mOpenFiles.indexOfValue(destination);
166         if (indexOfValue >= 0) {
167             mOpenFiles.removeAt(indexOfValue);
168         }
169     }
170 
171     /**
172      * Finishes the edit and commits all changes.
173      *
174      * @see #startWrite()
175      *
176      * @throws IllegalStateException is some files are not closed.
177      */
finishWrite()178     public void finishWrite() {
179         throwIfSomeFilesOpen();
180         fsyncDirectoryFd(mBaseDirectoryFd);
181         deleteDirectory(mBackupDirectory);
182         fsyncDirectoryFd(mBackupDirectoryFd);
183         mBaseDirectoryFd = -1;
184         mBackupDirectoryFd = -1;
185     }
186 
187     /**
188      * Finishes the edit and discards all changes.
189      *
190      * @see #startWrite()
191      */
failWrite()192     public void failWrite() {
193         throwIfSomeFilesOpen();
194         try{
195             restore();
196         } catch (IOException ignored) {}
197         mBaseDirectoryFd = -1;
198         mBackupDirectoryFd = -1;
199     }
200 
201     /**
202      * @return Whether this directory exists.
203      */
exists()204     public boolean exists() {
205         return mBaseDirectory.exists() || mBackupDirectory.exists();
206     }
207 
208     /**
209      * Deletes this directory.
210      */
delete()211     public void delete() {
212         if (mBaseDirectory.exists()) {
213             deleteDirectory(mBaseDirectory);
214             fsyncDirectoryFd(mBaseDirectoryFd);
215         }
216         if (mBackupDirectory.exists()) {
217             deleteDirectory(mBackupDirectory);
218             fsyncDirectoryFd(mBackupDirectoryFd);
219         }
220     }
221 
getOrCreateBaseDirectory()222     private @NonNull File getOrCreateBaseDirectory() throws IOException {
223         if (!mBaseDirectory.exists()) {
224             if (!mBaseDirectory.mkdirs()) {
225                 throw new IOException("Couldn't create directory " + mBaseDirectory);
226             }
227             FileUtils.setPermissions(mBaseDirectory.getPath(),
228                     FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH,
229                     -1, -1);
230         }
231         if (mBaseDirectoryFd < 0) {
232             mBaseDirectoryFd = getDirectoryFd(mBaseDirectory.getCanonicalPath());
233         }
234         return mBaseDirectory;
235     }
236 
throwIfSomeFilesOpen()237     private void throwIfSomeFilesOpen() {
238         if (!mOpenFiles.isEmpty()) {
239             throw new IllegalStateException("Unclosed files: "
240                     + Arrays.toString(mOpenFiles.keySet().toArray()));
241         }
242     }
243 
backup()244     private void backup() throws IOException {
245         if (!mBaseDirectory.exists()) {
246             return;
247         }
248         if (mBaseDirectoryFd < 0) {
249             mBaseDirectoryFd = getDirectoryFd(mBaseDirectory.getCanonicalPath());
250         }
251         if (mBackupDirectory.exists()) {
252             deleteDirectory(mBackupDirectory);
253         }
254         if (!mBaseDirectory.renameTo(mBackupDirectory)) {
255             throw new IOException("Couldn't backup " + mBaseDirectory
256                     + " to " + mBackupDirectory);
257         }
258         mBackupDirectoryFd = mBaseDirectoryFd;
259         mBaseDirectoryFd = -1;
260         fsyncDirectoryFd(mBackupDirectoryFd);
261     }
262 
restore()263     private void restore() throws IOException {
264         if (!mBackupDirectory.exists()) {
265             return;
266         }
267         if (mBackupDirectoryFd == -1) {
268             mBackupDirectoryFd = getDirectoryFd(mBackupDirectory.getCanonicalPath());
269         }
270         if (mBaseDirectory.exists()) {
271             deleteDirectory(mBaseDirectory);
272         }
273         if (!mBackupDirectory.renameTo(mBaseDirectory)) {
274             throw new IOException("Couldn't restore " + mBackupDirectory
275                     + " to " + mBaseDirectory);
276         }
277         mBaseDirectoryFd = mBackupDirectoryFd;
278         mBackupDirectoryFd = -1;
279         fsyncDirectoryFd(mBaseDirectoryFd);
280     }
281 
deleteDirectory(@onNull File file)282     private static void deleteDirectory(@NonNull File file) {
283         final File[] children = file.listFiles();
284         if (children != null) {
285             for (File child : children) {
286                 deleteDirectory(child);
287             }
288         }
289         file.delete();
290     }
291 
getDirectoryFd(String path)292     private static native int getDirectoryFd(String path);
fsyncDirectoryFd(int fd)293     private static native void fsyncDirectoryFd(int fd);
294 }
295