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.util.Log; 21 22 import java.io.File; 23 import java.io.FileInputStream; 24 import java.io.FileNotFoundException; 25 import java.io.FileOutputStream; 26 import java.io.IOException; 27 28 /** 29 * Helper class for performing atomic operations on a file by creating a 30 * backup file until a write has successfully completed. If you need this 31 * on older versions of the platform you can use 32 * {@link android.support.v4.util.AtomicFile} in the v4 support library. 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 */ 45 public 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(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 public File getBaseFile() { 63 return mBaseName; 64 } 65 66 /** 67 * Delete the atomic file. This deletes both the base and backup files. 68 */ 69 public void delete() { 70 mBaseName.delete(); 71 mBackupName.delete(); 72 } 73 74 /** 75 * Start a new write operation on the file. This returns a FileOutputStream 76 * to which you can write the new file data. The existing file is replaced 77 * with the new data. You <em>must not</em> directly close the given 78 * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} 79 * or {@link #failWrite(FileOutputStream)}. 80 * 81 * <p>Note that if another thread is currently performing 82 * a write, this will simply replace whatever that thread is writing 83 * with the new file being written by this thread, and when the other 84 * thread finishes the write the new write operation will no longer be 85 * safe (or will be lost). You must do your own threading protection for 86 * access to AtomicFile. 87 */ 88 public FileOutputStream startWrite() throws IOException { 89 // Rename the current file so it may be used as a backup during the next read 90 if (mBaseName.exists()) { 91 if (!mBackupName.exists()) { 92 if (!mBaseName.renameTo(mBackupName)) { 93 Log.w("AtomicFile", "Couldn't rename file " + mBaseName 94 + " to backup file " + mBackupName); 95 } 96 } else { 97 mBaseName.delete(); 98 } 99 } 100 FileOutputStream str = null; 101 try { 102 str = new FileOutputStream(mBaseName); 103 } catch (FileNotFoundException e) { 104 File parent = mBaseName.getParentFile(); 105 if (!parent.mkdir()) { 106 throw new IOException("Couldn't create directory " + mBaseName); 107 } 108 FileUtils.setPermissions( 109 parent.getPath(), 110 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, 111 -1, -1); 112 try { 113 str = new FileOutputStream(mBaseName); 114 } catch (FileNotFoundException e2) { 115 throw new IOException("Couldn't create " + mBaseName); 116 } 117 } 118 return str; 119 } 120 121 /** 122 * Call when you have successfully finished writing to the stream 123 * returned by {@link #startWrite()}. This will close, sync, and 124 * commit the new data. The next attempt to read the atomic file 125 * will return the new file stream. 126 */ 127 public void finishWrite(FileOutputStream str) { 128 if (str != null) { 129 FileUtils.sync(str); 130 try { 131 str.close(); 132 mBackupName.delete(); 133 } catch (IOException e) { 134 Log.w("AtomicFile", "finishWrite: Got exception:", e); 135 } 136 } 137 } 138 139 /** 140 * Call when you have failed for some reason at writing to the stream 141 * returned by {@link #startWrite()}. This will close the current 142 * write stream, and roll back to the previous state of the file. 143 */ 144 public void failWrite(FileOutputStream str) { 145 if (str != null) { 146 FileUtils.sync(str); 147 try { 148 str.close(); 149 mBaseName.delete(); 150 mBackupName.renameTo(mBaseName); 151 } catch (IOException e) { 152 Log.w("AtomicFile", "failWrite: Got exception:", e); 153 } 154 } 155 } 156 157 /** @hide 158 * @deprecated This is not safe. 159 */ 160 @Deprecated public void truncate() throws IOException { 161 try { 162 FileOutputStream fos = new FileOutputStream(mBaseName); 163 FileUtils.sync(fos); 164 fos.close(); 165 } catch (FileNotFoundException e) { 166 throw new IOException("Couldn't append " + mBaseName); 167 } catch (IOException e) { 168 } 169 } 170 171 /** @hide 172 * @deprecated This is not safe. 173 */ 174 @Deprecated public FileOutputStream openAppend() throws IOException { 175 try { 176 return new FileOutputStream(mBaseName, true); 177 } catch (FileNotFoundException e) { 178 throw new IOException("Couldn't append " + mBaseName); 179 } 180 } 181 182 /** 183 * Open the atomic file for reading. If there previously was an 184 * incomplete write, this will roll back to the last good data before 185 * opening for read. You should call close() on the FileInputStream when 186 * you are done reading from it. 187 * 188 * <p>Note that if another thread is currently performing 189 * a write, this will incorrectly consider it to be in the state of a bad 190 * write and roll back, causing the new data currently being written to 191 * be dropped. You must do your own threading protection for access to 192 * AtomicFile. 193 */ 194 public FileInputStream openRead() throws FileNotFoundException { 195 if (mBackupName.exists()) { 196 mBaseName.delete(); 197 mBackupName.renameTo(mBaseName); 198 } 199 return new FileInputStream(mBaseName); 200 } 201 202 /** 203 * A convenience for {@link #openRead()} that also reads all of the 204 * file contents into a byte array which is returned. 205 */ 206 public byte[] readFully() throws IOException { 207 FileInputStream stream = openRead(); 208 try { 209 int pos = 0; 210 int avail = stream.available(); 211 byte[] data = new byte[avail]; 212 while (true) { 213 int amt = stream.read(data, pos, data.length-pos); 214 //Log.i("foo", "Read " + amt + " bytes at " + pos 215 // + " of avail " + data.length); 216 if (amt <= 0) { 217 //Log.i("foo", "**** FINISHED READING: pos=" + pos 218 // + " len=" + data.length); 219 return data; 220 } 221 pos += amt; 222 avail = stream.available(); 223 if (avail > data.length-pos) { 224 byte[] newData = new byte[pos+avail]; 225 System.arraycopy(data, 0, newData, 0, pos); 226 data = newData; 227 } 228 } 229 } finally { 230 stream.close(); 231 } 232 } 233 } 234