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