AtomicFile.java revision b31c3281d870e9abb673db239234d580dcc4feff
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 17package androidx.core.util; 18 19import androidx.annotation.NonNull; 20import androidx.annotation.Nullable; 21import android.util.Log; 22 23import java.io.File; 24import java.io.FileInputStream; 25import java.io.FileNotFoundException; 26import java.io.FileOutputStream; 27import java.io.IOException; 28 29/** 30 * Static library support version of the framework's {@link android.util.AtomicFile}, 31 * a helper class for performing atomic operations on a file by creating a 32 * backup file until a write has successfully completed. 33 * <p> 34 * Atomic file guarantees file integrity by ensuring that a file has 35 * been completely written and sync'd to disk before removing its backup. 36 * As long as the backup file exists, the original file is considered 37 * to be invalid (left over from a previous attempt to write the file). 38 * </p><p> 39 * Atomic file does not confer any file locking semantics. 40 * Do not use this class when the file may be accessed or modified concurrently 41 * by multiple threads or processes. The caller is responsible for ensuring 42 * appropriate mutual exclusion invariants whenever it accesses the file. 43 * </p> 44 */ 45public class AtomicFile { 46 private final File mBaseName; 47 private final File mBackupName; 48 49 /** 50 * Create a new AtomicFile for a file located at the given File path. 51 * The secondary backup file will be the same file path with ".bak" appended. 52 */ 53 public AtomicFile(@NonNull File baseName) { 54 mBaseName = baseName; 55 mBackupName = new File(baseName.getPath() + ".bak"); 56 } 57 58 /** 59 * Return the path to the base file. You should not generally use this, 60 * as the data at that path may not be valid. 61 */ 62 @NonNull 63 public File getBaseFile() { 64 return mBaseName; 65 } 66 67 /** 68 * Delete the atomic file. This deletes both the base and backup files. 69 */ 70 public void delete() { 71 mBaseName.delete(); 72 mBackupName.delete(); 73 } 74 75 /** 76 * Start a new write operation on the file. This returns a FileOutputStream 77 * to which you can write the new file data. The existing file is replaced 78 * with the new data. You <em>must not</em> directly close the given 79 * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} 80 * or {@link #failWrite(FileOutputStream)}. 81 * 82 * <p>Note that if another thread is currently performing 83 * a write, this will simply replace whatever that thread is writing 84 * with the new file being written by this thread, and when the other 85 * thread finishes the write the new write operation will no longer be 86 * safe (or will be lost). You must do your own threading protection for 87 * access to AtomicFile. 88 */ 89 @NonNull 90 public FileOutputStream startWrite() throws IOException { 91 // Rename the current file so it may be used as a backup during the next read 92 if (mBaseName.exists()) { 93 if (!mBackupName.exists()) { 94 if (!mBaseName.renameTo(mBackupName)) { 95 Log.w("AtomicFile", "Couldn't rename file " + mBaseName 96 + " to backup file " + mBackupName); 97 } 98 } else { 99 mBaseName.delete(); 100 } 101 } 102 FileOutputStream str; 103 try { 104 str = new FileOutputStream(mBaseName); 105 } catch (FileNotFoundException e) { 106 File parent = mBaseName.getParentFile(); 107 if (!parent.mkdirs()) { 108 throw new IOException("Couldn't create directory " + mBaseName); 109 } 110 try { 111 str = new FileOutputStream(mBaseName); 112 } catch (FileNotFoundException e2) { 113 throw new IOException("Couldn't create " + mBaseName); 114 } 115 } 116 return str; 117 } 118 119 /** 120 * Call when you have successfully finished writing to the stream 121 * returned by {@link #startWrite()}. This will close, sync, and 122 * commit the new data. The next attempt to read the atomic file 123 * will return the new file stream. 124 */ 125 public void finishWrite(@Nullable FileOutputStream str) { 126 if (str != null) { 127 sync(str); 128 try { 129 str.close(); 130 mBackupName.delete(); 131 } catch (IOException e) { 132 Log.w("AtomicFile", "finishWrite: Got exception:", e); 133 } 134 } 135 } 136 137 /** 138 * Call when you have failed for some reason at writing to the stream 139 * returned by {@link #startWrite()}. This will close the current 140 * write stream, and roll back to the previous state of the file. 141 */ 142 public void failWrite(@Nullable FileOutputStream str) { 143 if (str != null) { 144 sync(str); 145 try { 146 str.close(); 147 mBaseName.delete(); 148 mBackupName.renameTo(mBaseName); 149 } catch (IOException e) { 150 Log.w("AtomicFile", "failWrite: Got exception:", e); 151 } 152 } 153 } 154 155 /** 156 * Open the atomic file for reading. If there previously was an 157 * incomplete write, this will roll back to the last good data before 158 * opening for read. You should call close() on the FileInputStream when 159 * you are done reading from it. 160 * 161 * <p>Note that if another thread is currently performing 162 * a write, this will incorrectly consider it to be in the state of a bad 163 * write and roll back, causing the new data currently being written to 164 * be dropped. You must do your own threading protection for access to 165 * AtomicFile. 166 */ 167 @NonNull 168 public FileInputStream openRead() throws FileNotFoundException { 169 if (mBackupName.exists()) { 170 mBaseName.delete(); 171 mBackupName.renameTo(mBaseName); 172 } 173 return new FileInputStream(mBaseName); 174 } 175 176 /** 177 * A convenience for {@link #openRead()} that also reads all of the 178 * file contents into a byte array which is returned. 179 */ 180 @NonNull 181 public byte[] readFully() throws IOException { 182 FileInputStream stream = openRead(); 183 try { 184 int pos = 0; 185 int avail = stream.available(); 186 byte[] data = new byte[avail]; 187 while (true) { 188 int amt = stream.read(data, pos, data.length-pos); 189 //Log.i("foo", "Read " + amt + " bytes at " + pos 190 // + " of avail " + data.length); 191 if (amt <= 0) { 192 //Log.i("foo", "**** FINISHED READING: pos=" + pos 193 // + " len=" + data.length); 194 return data; 195 } 196 pos += amt; 197 avail = stream.available(); 198 if (avail > data.length-pos) { 199 byte[] newData = new byte[pos+avail]; 200 System.arraycopy(data, 0, newData, 0, pos); 201 data = newData; 202 } 203 } 204 } finally { 205 stream.close(); 206 } 207 } 208 209 private static boolean sync(@NonNull FileOutputStream stream) { 210 try { 211 stream.getFD().sync(); 212 return true; 213 } catch (IOException e) { 214 } 215 return false; 216 } 217} 218