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