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 21 import libcore.io.IoUtils; 22 23 import java.io.File; 24 import java.io.FileInputStream; 25 import java.io.FileNotFoundException; 26 import java.io.FileOutputStream; 27 import java.io.IOException; 28 import java.util.function.Consumer; 29 30 /** 31 * Helper class for performing atomic operations on a file by creating a 32 * backup file until a write has successfully completed. If you need this 33 * on older versions of the platform you can use 34 * {@link android.support.v4.util.AtomicFile} in the v4 support library. 35 * <p> 36 * Atomic file guarantees file integrity by ensuring that a file has 37 * been completely written and sync'd to disk before removing its backup. 38 * As long as the backup file exists, the original file is considered 39 * to be invalid (left over from a previous attempt to write the file). 40 * </p><p> 41 * Atomic file does not confer any file locking semantics. 42 * Do not use this class when the file may be accessed or modified concurrently 43 * by multiple threads or processes. The caller is responsible for ensuring 44 * appropriate mutual exclusion invariants whenever it accesses the file. 45 * </p> 46 */ 47 public class AtomicFile { 48 private final File mBaseName; 49 private final File mBackupName; 50 51 /** 52 * Create a new AtomicFile for a file located at the given File path. 53 * The secondary backup file will be the same file path with ".bak" appended. 54 */ AtomicFile(File baseName)55 public AtomicFile(File baseName) { 56 mBaseName = baseName; 57 mBackupName = new File(baseName.getPath() + ".bak"); 58 } 59 60 /** 61 * Return the path to the base file. You should not generally use this, 62 * as the data at that path may not be valid. 63 */ getBaseFile()64 public File getBaseFile() { 65 return mBaseName; 66 } 67 68 /** 69 * Delete the atomic file. This deletes both the base and backup files. 70 */ delete()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 */ startWrite()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 = null; 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 FileUtils.setPermissions( 111 parent.getPath(), 112 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, 113 -1, -1); 114 try { 115 str = new FileOutputStream(mBaseName); 116 } catch (FileNotFoundException e2) { 117 throw new IOException("Couldn't create " + mBaseName); 118 } 119 } 120 return str; 121 } 122 123 /** 124 * Call when you have successfully finished writing to the stream 125 * returned by {@link #startWrite()}. This will close, sync, and 126 * commit the new data. The next attempt to read the atomic file 127 * will return the new file stream. 128 */ finishWrite(FileOutputStream str)129 public void finishWrite(FileOutputStream str) { 130 if (str != null) { 131 FileUtils.sync(str); 132 try { 133 str.close(); 134 mBackupName.delete(); 135 } catch (IOException e) { 136 Log.w("AtomicFile", "finishWrite: Got exception:", e); 137 } 138 } 139 } 140 141 /** 142 * Call when you have failed for some reason at writing to the stream 143 * returned by {@link #startWrite()}. This will close the current 144 * write stream, and roll back to the previous state of the file. 145 */ failWrite(FileOutputStream str)146 public void failWrite(FileOutputStream str) { 147 if (str != null) { 148 FileUtils.sync(str); 149 try { 150 str.close(); 151 mBaseName.delete(); 152 mBackupName.renameTo(mBaseName); 153 } catch (IOException e) { 154 Log.w("AtomicFile", "failWrite: Got exception:", e); 155 } 156 } 157 } 158 159 /** @hide 160 * @deprecated This is not safe. 161 */ truncate()162 @Deprecated public void truncate() throws IOException { 163 try { 164 FileOutputStream fos = new FileOutputStream(mBaseName); 165 FileUtils.sync(fos); 166 fos.close(); 167 } catch (FileNotFoundException e) { 168 throw new IOException("Couldn't append " + mBaseName); 169 } catch (IOException e) { 170 } 171 } 172 173 /** @hide 174 * @deprecated This is not safe. 175 */ openAppend()176 @Deprecated public FileOutputStream openAppend() throws IOException { 177 try { 178 return new FileOutputStream(mBaseName, true); 179 } catch (FileNotFoundException e) { 180 throw new IOException("Couldn't append " + mBaseName); 181 } 182 } 183 184 /** 185 * Open the atomic file for reading. If there previously was an 186 * incomplete write, this will roll back to the last good data before 187 * opening for read. You should call close() on the FileInputStream when 188 * you are done reading from it. 189 * 190 * <p>Note that if another thread is currently performing 191 * a write, this will incorrectly consider it to be in the state of a bad 192 * write and roll back, causing the new data currently being written to 193 * be dropped. You must do your own threading protection for access to 194 * AtomicFile. 195 */ openRead()196 public FileInputStream openRead() throws FileNotFoundException { 197 if (mBackupName.exists()) { 198 mBaseName.delete(); 199 mBackupName.renameTo(mBaseName); 200 } 201 return new FileInputStream(mBaseName); 202 } 203 204 /** 205 * Gets the last modified time of the atomic file. 206 * {@hide} 207 * 208 * @return last modified time in milliseconds since epoch. 209 * @throws IOException 210 */ getLastModifiedTime()211 public long getLastModifiedTime() throws IOException { 212 if (mBackupName.exists()) { 213 return mBackupName.lastModified(); 214 } 215 return mBaseName.lastModified(); 216 } 217 218 /** 219 * A convenience for {@link #openRead()} that also reads all of the 220 * file contents into a byte array which is returned. 221 */ readFully()222 public byte[] readFully() throws IOException { 223 FileInputStream stream = openRead(); 224 try { 225 int pos = 0; 226 int avail = stream.available(); 227 byte[] data = new byte[avail]; 228 while (true) { 229 int amt = stream.read(data, pos, data.length-pos); 230 //Log.i("foo", "Read " + amt + " bytes at " + pos 231 // + " of avail " + data.length); 232 if (amt <= 0) { 233 //Log.i("foo", "**** FINISHED READING: pos=" + pos 234 // + " len=" + data.length); 235 return data; 236 } 237 pos += amt; 238 avail = stream.available(); 239 if (avail > data.length-pos) { 240 byte[] newData = new byte[pos+avail]; 241 System.arraycopy(data, 0, newData, 0, pos); 242 data = newData; 243 } 244 } 245 } finally { 246 stream.close(); 247 } 248 } 249 250 /** @hide */ write(Consumer<FileOutputStream> writeContent)251 public void write(Consumer<FileOutputStream> writeContent) { 252 FileOutputStream out = null; 253 try { 254 out = startWrite(); 255 writeContent.accept(out); 256 finishWrite(out); 257 } catch (Throwable t) { 258 failWrite(out); 259 throw ExceptionUtils.propagate(t); 260 } finally { 261 IoUtils.closeQuietly(out); 262 } 263 } 264 } 265