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