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