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