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