1 /* 2 * Copyright (C) 2009 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 android.util; 18 19 import android.annotation.CurrentTimeMillisLong; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SuppressLint; 23 import android.annotation.SystemApi; 24 import android.os.FileUtils; 25 import android.os.SystemClock; 26 27 import libcore.io.IoUtils; 28 29 import java.io.File; 30 import java.io.FileInputStream; 31 import java.io.FileNotFoundException; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.util.function.Consumer; 35 36 /** 37 * Helper class for performing atomic operations on a file by writing to a new file and renaming it 38 * into the place of the original file after the write has successfully completed. If you need this 39 * on older versions of the platform you can use {@link androidx.core.util.AtomicFile} in AndroidX. 40 * <p> 41 * Atomic file guarantees file integrity by ensuring that a file has been completely written and 42 * sync'd to disk before renaming it to the original file. Previously this is done by renaming the 43 * original file to a backup file beforehand, but this approach couldn't handle the case where the 44 * file is created for the first time. This class will also handle the backup file created by the 45 * old implementation properly. 46 * <p> 47 * Atomic file does not confer any file locking semantics. Do not use this class when the file may 48 * be accessed or modified concurrently by multiple threads or processes. The caller is responsible 49 * for ensuring appropriate mutual exclusion invariants whenever it accesses the file. 50 */ 51 public class AtomicFile { 52 private static final String LOG_TAG = "AtomicFile"; 53 54 private final File mBaseName; 55 private final File mNewName; 56 private final File mLegacyBackupName; 57 private SystemConfigFileCommitEventLogger mCommitEventLogger; 58 59 /** 60 * Create a new AtomicFile for a file located at the given File path. 61 * The new file created when writing will be the same file path with ".new" appended. 62 */ AtomicFile(File baseName)63 public AtomicFile(File baseName) { 64 this(baseName, (SystemConfigFileCommitEventLogger) null); 65 } 66 67 /** 68 * @hide Internal constructor that also allows you to have the class 69 * automatically log commit events. 70 */ AtomicFile(File baseName, String commitTag)71 public AtomicFile(File baseName, String commitTag) { 72 this(baseName, new SystemConfigFileCommitEventLogger(commitTag)); 73 } 74 75 /** 76 * Internal constructor that also allows you to have the class 77 * automatically log commit events. 78 * 79 * @hide 80 */ 81 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) 82 @SuppressLint("StreamFiles") AtomicFile(@onNull File baseName, @Nullable SystemConfigFileCommitEventLogger commitEventLogger)83 public AtomicFile(@NonNull File baseName, 84 @Nullable SystemConfigFileCommitEventLogger commitEventLogger) { 85 mBaseName = baseName; 86 mNewName = new File(baseName.getPath() + ".new"); 87 mLegacyBackupName = new File(baseName.getPath() + ".bak"); 88 mCommitEventLogger = commitEventLogger; 89 } 90 91 /** 92 * Return the path to the base file. You should not generally use this, 93 * as the data at that path may not be valid. 94 */ getBaseFile()95 public File getBaseFile() { 96 return mBaseName; 97 } 98 99 /** 100 * Delete the atomic file. This deletes both the base and new files. 101 */ delete()102 public void delete() { 103 mBaseName.delete(); 104 mNewName.delete(); 105 mLegacyBackupName.delete(); 106 } 107 108 /** 109 * Start a new write operation on the file. This returns a FileOutputStream 110 * to which you can write the new file data. The existing file is replaced 111 * with the new data. You <em>must not</em> directly close the given 112 * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} 113 * or {@link #failWrite(FileOutputStream)}. 114 * 115 * <p>Note that if another thread is currently performing 116 * a write, this will simply replace whatever that thread is writing 117 * with the new file being written by this thread, and when the other 118 * thread finishes the write the new write operation will no longer be 119 * safe (or will be lost). You must do your own threading protection for 120 * access to AtomicFile. 121 */ startWrite()122 public FileOutputStream startWrite() throws IOException { 123 return startWrite(0); 124 } 125 126 /** 127 * @hide Internal version of {@link #startWrite()} that allows you to specify an earlier 128 * start time of the operation to adjust how the commit is logged. 129 * @param startTime The effective start time of the operation, in the time 130 * base of {@link SystemClock#uptimeMillis()}. 131 * 132 * @deprecated Use {@link SystemConfigFileCommitEventLogger#setStartTime} followed 133 * by {@link #startWrite()} 134 */ 135 @Deprecated startWrite(long startTime)136 public FileOutputStream startWrite(long startTime) throws IOException { 137 if (mCommitEventLogger != null) { 138 if (startTime != 0) { 139 mCommitEventLogger.setStartTime(startTime); 140 } 141 142 mCommitEventLogger.onStartWrite(); 143 } 144 145 if (mLegacyBackupName.exists()) { 146 rename(mLegacyBackupName, mBaseName); 147 } 148 149 try { 150 return new FileOutputStream(mNewName); 151 } catch (FileNotFoundException e) { 152 File parent = mNewName.getParentFile(); 153 if (!parent.mkdirs()) { 154 throw new IOException("Failed to create directory for " + mNewName); 155 } 156 FileUtils.setPermissions(parent.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG 157 | FileUtils.S_IXOTH, -1, -1); 158 try { 159 return new FileOutputStream(mNewName); 160 } catch (FileNotFoundException e2) { 161 throw new IOException("Failed to create new file " + mNewName, e2); 162 } 163 } 164 } 165 166 /** 167 * Call when you have successfully finished writing to the stream 168 * returned by {@link #startWrite()}. This will close, sync, and 169 * commit the new data. The next attempt to read the atomic file 170 * will return the new file stream. 171 */ finishWrite(FileOutputStream str)172 public void finishWrite(FileOutputStream str) { 173 if (str == null) { 174 return; 175 } 176 if (!FileUtils.sync(str)) { 177 Log.e(LOG_TAG, "Failed to sync file output stream"); 178 } 179 try { 180 str.close(); 181 } catch (IOException e) { 182 Log.e(LOG_TAG, "Failed to close file output stream", e); 183 } 184 rename(mNewName, mBaseName); 185 if (mCommitEventLogger != null) { 186 mCommitEventLogger.onFinishWrite(); 187 } 188 } 189 190 /** 191 * Call when you have failed for some reason at writing to the stream 192 * returned by {@link #startWrite()}. This will close the current 193 * write stream, and delete the new file. 194 */ failWrite(FileOutputStream str)195 public void failWrite(FileOutputStream str) { 196 if (str == null) { 197 return; 198 } 199 if (!FileUtils.sync(str)) { 200 Log.e(LOG_TAG, "Failed to sync file output stream"); 201 } 202 try { 203 str.close(); 204 } catch (IOException e) { 205 Log.e(LOG_TAG, "Failed to close file output stream", e); 206 } 207 if (!mNewName.delete()) { 208 Log.e(LOG_TAG, "Failed to delete new file " + mNewName); 209 } 210 } 211 212 /** @hide 213 * @deprecated This is not safe. 214 */ truncate()215 @Deprecated public void truncate() throws IOException { 216 try { 217 FileOutputStream fos = new FileOutputStream(mBaseName); 218 FileUtils.sync(fos); 219 fos.close(); 220 } catch (FileNotFoundException e) { 221 throw new IOException("Couldn't append " + mBaseName); 222 } catch (IOException e) { 223 } 224 } 225 226 /** @hide 227 * @deprecated This is not safe. 228 */ openAppend()229 @Deprecated public FileOutputStream openAppend() throws IOException { 230 try { 231 return new FileOutputStream(mBaseName, true); 232 } catch (FileNotFoundException e) { 233 throw new IOException("Couldn't append " + mBaseName); 234 } 235 } 236 237 /** 238 * Open the atomic file for reading. You should call close() on the FileInputStream when you are 239 * done reading from it. 240 * <p> 241 * You must do your own threading protection for access to AtomicFile. 242 */ openRead()243 public FileInputStream openRead() throws FileNotFoundException { 244 if (mLegacyBackupName.exists()) { 245 rename(mLegacyBackupName, mBaseName); 246 } 247 248 // It was okay to call openRead() between startWrite() and finishWrite() for the first time 249 // (because there is no backup file), where openRead() would open the file being written, 250 // which makes no sense, but finishWrite() would still persist the write properly. For all 251 // subsequent writes, if openRead() was called in between, it would see a backup file and 252 // delete the file being written, the same behavior as our new implementation. So we only 253 // need a special case for the first write, and don't delete the new file in this case so 254 // that finishWrite() can still work. 255 if (mNewName.exists() && mBaseName.exists()) { 256 if (!mNewName.delete()) { 257 Log.e(LOG_TAG, "Failed to delete outdated new file " + mNewName); 258 } 259 } 260 return new FileInputStream(mBaseName); 261 } 262 263 /** 264 * @hide 265 * Checks if the original or legacy backup file exists. 266 * @return whether the original or legacy backup file exists. 267 */ exists()268 public boolean exists() { 269 return mBaseName.exists() || mLegacyBackupName.exists(); 270 } 271 272 /** 273 * Gets the last modified time of the atomic file. 274 * 275 * @return last modified time in milliseconds since epoch. Returns zero if 276 * the file does not exist or an I/O error is encountered. 277 */ 278 @CurrentTimeMillisLong getLastModifiedTime()279 public long getLastModifiedTime() { 280 if (mLegacyBackupName.exists()) { 281 return mLegacyBackupName.lastModified(); 282 } 283 return mBaseName.lastModified(); 284 } 285 286 /** 287 * A convenience for {@link #openRead()} that also reads all of the 288 * file contents into a byte array which is returned. 289 */ readFully()290 public byte[] readFully() throws IOException { 291 FileInputStream stream = openRead(); 292 try { 293 int pos = 0; 294 int avail = stream.available(); 295 byte[] data = new byte[avail]; 296 while (true) { 297 int amt = stream.read(data, pos, data.length-pos); 298 //Log.i("foo", "Read " + amt + " bytes at " + pos 299 // + " of avail " + data.length); 300 if (amt <= 0) { 301 //Log.i("foo", "**** FINISHED READING: pos=" + pos 302 // + " len=" + data.length); 303 return data; 304 } 305 pos += amt; 306 avail = stream.available(); 307 if (avail > data.length-pos) { 308 byte[] newData = new byte[pos+avail]; 309 System.arraycopy(data, 0, newData, 0, pos); 310 data = newData; 311 } 312 } 313 } finally { 314 stream.close(); 315 } 316 } 317 318 /** @hide */ write(Consumer<FileOutputStream> writeContent)319 public void write(Consumer<FileOutputStream> writeContent) { 320 FileOutputStream out = null; 321 try { 322 out = startWrite(); 323 writeContent.accept(out); 324 finishWrite(out); 325 } catch (Throwable t) { 326 failWrite(out); 327 throw ExceptionUtils.propagate(t); 328 } finally { 329 IoUtils.closeQuietly(out); 330 } 331 } 332 rename(File source, File target)333 private static void rename(File source, File target) { 334 // We used to delete the target file before rename, but that isn't atomic, and the rename() 335 // syscall should atomically replace the target file. However in the case where the target 336 // file is a directory, a simple rename() won't work. We need to delete the file in this 337 // case because there are callers who erroneously called mBaseName.mkdirs() (instead of 338 // mBaseName.getParentFile().mkdirs()) before creating the AtomicFile, and it worked 339 // regardless, so this deletion became some kind of API. 340 if (target.isDirectory()) { 341 if (!target.delete()) { 342 Log.e(LOG_TAG, "Failed to delete file which is a directory " + target); 343 } 344 } 345 if (!source.renameTo(target)) { 346 Log.e(LOG_TAG, "Failed to rename " + source + " to " + target); 347 } 348 } 349 } 350