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 androidx.core.util; 18 19 import android.util.Log; 20 21 import org.jspecify.annotations.NonNull; 22 import org.jspecify.annotations.Nullable; 23 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.FileNotFoundException; 27 import java.io.FileOutputStream; 28 import java.io.IOException; 29 30 /** 31 * Static library support version of the framework's {@link android.util.AtomicFile}, a helper class 32 * for performing atomic operations on a file by writing to a new file and renaming it into the 33 * place of the original file after the write has successfully completed. 34 * <p> 35 * Atomic file guarantees file integrity by ensuring that a file has been completely written and 36 * sync'd to disk before renaming it to the original file. Previously this is done by renaming the 37 * original file to a backup file beforehand, but this approach couldn't handle the case where the 38 * file is created for the first time. This class will also handle the backup file created by the 39 * old implementation properly. 40 * <p> 41 * Atomic file does not confer any file locking semantics. Do not use this class when the file may 42 * be accessed or modified concurrently by multiple threads or processes. The caller is responsible 43 * for ensuring appropriate mutual exclusion invariants whenever it accesses the file. 44 */ 45 public class AtomicFile { 46 private static final String LOG_TAG = "AtomicFile"; 47 48 private final File mBaseName; 49 private final File mNewName; 50 private final File mLegacyBackupName; 51 52 /** 53 * Create a new AtomicFile for a file located at the given File path. 54 * The new file created when writing will be the same file path with ".new" appended. 55 */ AtomicFile(@onNull File baseName)56 public AtomicFile(@NonNull File baseName) { 57 mBaseName = baseName; 58 mNewName = new File(baseName.getPath() + ".new"); 59 mLegacyBackupName = new File(baseName.getPath() + ".bak"); 60 } 61 62 /** 63 * Return the path to the base file. You should not generally use this, 64 * as the data at that path may not be valid. 65 */ getBaseFile()66 public @NonNull File getBaseFile() { 67 return mBaseName; 68 } 69 70 /** 71 * Delete the atomic file. This deletes both the base and new files. 72 */ delete()73 public void delete() { 74 mBaseName.delete(); 75 mNewName.delete(); 76 mLegacyBackupName.delete(); 77 } 78 79 /** 80 * Start a new write operation on the file. This returns a FileOutputStream 81 * to which you can write the new file data. The existing file is replaced 82 * with the new data. You <em>must not</em> directly close the given 83 * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} 84 * or {@link #failWrite(FileOutputStream)}. 85 * 86 * <p>Note that if another thread is currently performing 87 * a write, this will simply replace whatever that thread is writing 88 * with the new file being written by this thread, and when the other 89 * thread finishes the write the new write operation will no longer be 90 * safe (or will be lost). You must do your own threading protection for 91 * access to AtomicFile. 92 */ startWrite()93 public @NonNull FileOutputStream startWrite() throws IOException { 94 if (mLegacyBackupName.exists()) { 95 rename(mLegacyBackupName, mBaseName); 96 } 97 98 try { 99 return new FileOutputStream(mNewName); 100 } catch (FileNotFoundException e) { 101 File parent = mNewName.getParentFile(); 102 if (!parent.mkdirs()) { 103 throw new IOException("Failed to create directory for " + mNewName); 104 } 105 try { 106 return new FileOutputStream(mNewName); 107 } catch (FileNotFoundException e2) { 108 throw new IOException("Failed to create new file " + mNewName, e2); 109 } 110 } 111 } 112 113 /** 114 * Call when you have successfully finished writing to the stream 115 * returned by {@link #startWrite()}. This will close, sync, and 116 * commit the new data. The next attempt to read the atomic file 117 * will return the new file stream. 118 */ finishWrite(@ullable FileOutputStream str)119 public void finishWrite(@Nullable FileOutputStream str) { 120 if (str == null) { 121 return; 122 } 123 if (!sync(str)) { 124 Log.e(LOG_TAG, "Failed to sync file output stream"); 125 } 126 try { 127 str.close(); 128 } catch (IOException e) { 129 Log.e(LOG_TAG, "Failed to close file output stream", e); 130 } 131 rename(mNewName, mBaseName); 132 } 133 134 /** 135 * Call when you have failed for some reason at writing to the stream 136 * returned by {@link #startWrite()}. This will close the current 137 * write stream, and delete the new file. 138 */ failWrite(@ullable FileOutputStream str)139 public void failWrite(@Nullable FileOutputStream str) { 140 if (str == null) { 141 return; 142 } 143 if (!sync(str)) { 144 Log.e(LOG_TAG, "Failed to sync file output stream"); 145 } 146 try { 147 str.close(); 148 } catch (IOException e) { 149 Log.e(LOG_TAG, "Failed to close file output stream", e); 150 } 151 if (!mNewName.delete()) { 152 Log.e(LOG_TAG, "Failed to delete new file " + mNewName); 153 } 154 } 155 156 /** 157 * Open the atomic file for reading. You should call close() on the FileInputStream when you are 158 * done reading from it. 159 * <p> 160 * You must do your own threading protection for access to AtomicFile. 161 */ openRead()162 public @NonNull FileInputStream openRead() throws FileNotFoundException { 163 if (mLegacyBackupName.exists()) { 164 rename(mLegacyBackupName, mBaseName); 165 } 166 167 // It was okay to call openRead() between startWrite() and finishWrite() for the first time 168 // (because there is no backup file), where openRead() would open the file being written, 169 // which makes no sense, but finishWrite() would still persist the write properly. For all 170 // subsequent writes, if openRead() was called in between, it would see a backup file and 171 // delete the file being written, the same behavior as our new implementation. So we only 172 // need a special case for the first write, and don't delete the new file in this case so 173 // that finishWrite() can still work. 174 if (mNewName.exists() && mBaseName.exists()) { 175 if (!mNewName.delete()) { 176 Log.e(LOG_TAG, "Failed to delete outdated new file " + mNewName); 177 } 178 } 179 return new FileInputStream(mBaseName); 180 } 181 182 /** 183 * A convenience for {@link #openRead()} that also reads all of the 184 * file contents into a byte array which is returned. 185 */ readFully()186 public byte @NonNull [] readFully() throws IOException { 187 FileInputStream stream = openRead(); 188 try { 189 int pos = 0; 190 int avail = stream.available(); 191 byte[] data = new byte[avail]; 192 while (true) { 193 int amt = stream.read(data, pos, data.length-pos); 194 //Log.i("foo", "Read " + amt + " bytes at " + pos 195 // + " of avail " + data.length); 196 if (amt <= 0) { 197 //Log.i("foo", "**** FINISHED READING: pos=" + pos 198 // + " len=" + data.length); 199 return data; 200 } 201 pos += amt; 202 avail = stream.available(); 203 if (avail > data.length-pos) { 204 byte[] newData = new byte[pos+avail]; 205 System.arraycopy(data, 0, newData, 0, pos); 206 data = newData; 207 } 208 } 209 } finally { 210 stream.close(); 211 } 212 } 213 sync(@onNull FileOutputStream stream)214 private static boolean sync(@NonNull FileOutputStream stream) { 215 try { 216 stream.getFD().sync(); 217 return true; 218 } catch (IOException e) { 219 } 220 return false; 221 } 222 rename(@onNull File source, @NonNull File target)223 private static void rename(@NonNull File source, @NonNull File target) { 224 // We used to delete the target file before rename, but that isn't atomic, and the rename() 225 // syscall should atomically replace the target file. However in the case where the target 226 // file is a directory, a simple rename() won't work. We need to delete the file in this 227 // case because there are callers who erroneously called mBaseName.mkdirs() (instead of 228 // mBaseName.getParentFile().mkdirs()) before creating the AtomicFile, and it worked 229 // regardless, so this deletion became some kind of API. 230 if (target.isDirectory()) { 231 if (!target.delete()) { 232 Log.e(LOG_TAG, "Failed to delete file which is a directory " + target); 233 } 234 } 235 if (!source.renameTo(target)) { 236 Log.e(LOG_TAG, "Failed to rename " + source + " to " + target); 237 } 238 } 239 } 240