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