• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.CurrentTimeMillisLong;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SuppressLint;
23 import android.annotation.SystemApi;
24 import android.os.FileUtils;
25 import android.os.SystemClock;
26 
27 import libcore.io.IoUtils;
28 
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.FileNotFoundException;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.util.function.Consumer;
35 
36 /**
37  * Helper class for performing atomic operations on a file by writing to a new file and renaming it
38  * into the place of the original file after the write has successfully completed. If you need this
39  * on older versions of the platform you can use {@link androidx.core.util.AtomicFile} in AndroidX.
40  * <p>
41  * Atomic file guarantees file integrity by ensuring that a file has been completely written and
42  * sync'd to disk before renaming it to the original file. Previously this is done by renaming the
43  * original file to a backup file beforehand, but this approach couldn't handle the case where the
44  * file is created for the first time. This class will also handle the backup file created by the
45  * old implementation properly.
46  * <p>
47  * Atomic file does not confer any file locking semantics. Do not use this class when the file may
48  * be accessed or modified concurrently by multiple threads or processes. The caller is responsible
49  * for ensuring appropriate mutual exclusion invariants whenever it accesses the file.
50  */
51 public class AtomicFile {
52     private static final String LOG_TAG = "AtomicFile";
53 
54     private final File mBaseName;
55     private final File mNewName;
56     private final File mLegacyBackupName;
57     private SystemConfigFileCommitEventLogger mCommitEventLogger;
58 
59     /**
60      * Create a new AtomicFile for a file located at the given File path.
61      * The new file created when writing will be the same file path with ".new" appended.
62      */
AtomicFile(File baseName)63     public AtomicFile(File baseName) {
64         this(baseName, (SystemConfigFileCommitEventLogger) null);
65     }
66 
67     /**
68      * @hide Internal constructor that also allows you to have the class
69      * automatically log commit events.
70      */
AtomicFile(File baseName, String commitTag)71     public AtomicFile(File baseName, String commitTag) {
72         this(baseName, new SystemConfigFileCommitEventLogger(commitTag));
73     }
74 
75     /**
76      * Internal constructor that also allows you to have the class
77      * automatically log commit events.
78      *
79      * @hide
80      */
81     @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
82     @SuppressLint("StreamFiles")
AtomicFile(@onNull File baseName, @Nullable SystemConfigFileCommitEventLogger commitEventLogger)83     public AtomicFile(@NonNull File baseName,
84             @Nullable SystemConfigFileCommitEventLogger commitEventLogger) {
85         mBaseName = baseName;
86         mNewName = new File(baseName.getPath() + ".new");
87         mLegacyBackupName = new File(baseName.getPath() + ".bak");
88         mCommitEventLogger = commitEventLogger;
89     }
90 
91     /**
92      * Return the path to the base file.  You should not generally use this,
93      * as the data at that path may not be valid.
94      */
getBaseFile()95     public File getBaseFile() {
96         return mBaseName;
97     }
98 
99     /**
100      * Delete the atomic file.  This deletes both the base and new files.
101      */
delete()102     public void delete() {
103         mBaseName.delete();
104         mNewName.delete();
105         mLegacyBackupName.delete();
106     }
107 
108     /**
109      * Start a new write operation on the file.  This returns a FileOutputStream
110      * to which you can write the new file data.  The existing file is replaced
111      * with the new data.  You <em>must not</em> directly close the given
112      * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
113      * or {@link #failWrite(FileOutputStream)}.
114      *
115      * <p>Note that if another thread is currently performing
116      * a write, this will simply replace whatever that thread is writing
117      * with the new file being written by this thread, and when the other
118      * thread finishes the write the new write operation will no longer be
119      * safe (or will be lost).  You must do your own threading protection for
120      * access to AtomicFile.
121      */
startWrite()122     public FileOutputStream startWrite() throws IOException {
123         return startWrite(0);
124     }
125 
126     /**
127      * @hide Internal version of {@link #startWrite()} that allows you to specify an earlier
128      * start time of the operation to adjust how the commit is logged.
129      * @param startTime The effective start time of the operation, in the time
130      * base of {@link SystemClock#uptimeMillis()}.
131      *
132      * @deprecated Use {@link SystemConfigFileCommitEventLogger#setStartTime} followed
133      * by {@link #startWrite()}
134      */
135     @Deprecated
startWrite(long startTime)136     public FileOutputStream startWrite(long startTime) throws IOException {
137         if (mCommitEventLogger != null) {
138             if (startTime != 0) {
139                 mCommitEventLogger.setStartTime(startTime);
140             }
141 
142             mCommitEventLogger.onStartWrite();
143         }
144 
145         if (mLegacyBackupName.exists()) {
146             rename(mLegacyBackupName, mBaseName);
147         }
148 
149         try {
150             return new FileOutputStream(mNewName);
151         } catch (FileNotFoundException e) {
152             File parent = mNewName.getParentFile();
153             if (!parent.mkdirs()) {
154                 throw new IOException("Failed to create directory for " + mNewName);
155             }
156             FileUtils.setPermissions(parent.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG
157                     | FileUtils.S_IXOTH, -1, -1);
158             try {
159                 return new FileOutputStream(mNewName);
160             } catch (FileNotFoundException e2) {
161                 throw new IOException("Failed to create new file " + mNewName, e2);
162             }
163         }
164     }
165 
166     /**
167      * Call when you have successfully finished writing to the stream
168      * returned by {@link #startWrite()}.  This will close, sync, and
169      * commit the new data.  The next attempt to read the atomic file
170      * will return the new file stream.
171      */
finishWrite(FileOutputStream str)172     public void finishWrite(FileOutputStream str) {
173         if (str == null) {
174             return;
175         }
176         if (!FileUtils.sync(str)) {
177             Log.e(LOG_TAG, "Failed to sync file output stream");
178         }
179         try {
180             str.close();
181         } catch (IOException e) {
182             Log.e(LOG_TAG, "Failed to close file output stream", e);
183         }
184         rename(mNewName, mBaseName);
185         if (mCommitEventLogger != null) {
186             mCommitEventLogger.onFinishWrite();
187         }
188     }
189 
190     /**
191      * Call when you have failed for some reason at writing to the stream
192      * returned by {@link #startWrite()}.  This will close the current
193      * write stream, and delete the new file.
194      */
failWrite(FileOutputStream str)195     public void failWrite(FileOutputStream str) {
196         if (str == null) {
197             return;
198         }
199         if (!FileUtils.sync(str)) {
200             Log.e(LOG_TAG, "Failed to sync file output stream");
201         }
202         try {
203             str.close();
204         } catch (IOException e) {
205             Log.e(LOG_TAG, "Failed to close file output stream", e);
206         }
207         if (!mNewName.delete()) {
208             Log.e(LOG_TAG, "Failed to delete new file " + mNewName);
209         }
210     }
211 
212     /** @hide
213      * @deprecated This is not safe.
214      */
truncate()215     @Deprecated public void truncate() throws IOException {
216         try {
217             FileOutputStream fos = new FileOutputStream(mBaseName);
218             FileUtils.sync(fos);
219             fos.close();
220         } catch (FileNotFoundException e) {
221             throw new IOException("Couldn't append " + mBaseName);
222         } catch (IOException e) {
223         }
224     }
225 
226     /** @hide
227      * @deprecated This is not safe.
228      */
openAppend()229     @Deprecated public FileOutputStream openAppend() throws IOException {
230         try {
231             return new FileOutputStream(mBaseName, true);
232         } catch (FileNotFoundException e) {
233             throw new IOException("Couldn't append " + mBaseName);
234         }
235     }
236 
237     /**
238      * Open the atomic file for reading. You should call close() on the FileInputStream when you are
239      * done reading from it.
240      * <p>
241      * You must do your own threading protection for access to AtomicFile.
242      */
openRead()243     public FileInputStream openRead() throws FileNotFoundException {
244         if (mLegacyBackupName.exists()) {
245             rename(mLegacyBackupName, mBaseName);
246         }
247 
248         // It was okay to call openRead() between startWrite() and finishWrite() for the first time
249         // (because there is no backup file), where openRead() would open the file being written,
250         // which makes no sense, but finishWrite() would still persist the write properly. For all
251         // subsequent writes, if openRead() was called in between, it would see a backup file and
252         // delete the file being written, the same behavior as our new implementation. So we only
253         // need a special case for the first write, and don't delete the new file in this case so
254         // that finishWrite() can still work.
255         if (mNewName.exists() && mBaseName.exists()) {
256             if (!mNewName.delete()) {
257                 Log.e(LOG_TAG, "Failed to delete outdated new file " + mNewName);
258             }
259         }
260         return new FileInputStream(mBaseName);
261     }
262 
263     /**
264      * @hide
265      * Checks if the original or legacy backup file exists.
266      * @return whether the original or legacy backup file exists.
267      */
exists()268     public boolean exists() {
269         return mBaseName.exists() || mLegacyBackupName.exists();
270     }
271 
272     /**
273      * Gets the last modified time of the atomic file.
274      *
275      * @return last modified time in milliseconds since epoch.  Returns zero if
276      *     the file does not exist or an I/O error is encountered.
277      */
278     @CurrentTimeMillisLong
getLastModifiedTime()279     public long getLastModifiedTime() {
280         if (mLegacyBackupName.exists()) {
281             return mLegacyBackupName.lastModified();
282         }
283         return mBaseName.lastModified();
284     }
285 
286     /**
287      * A convenience for {@link #openRead()} that also reads all of the
288      * file contents into a byte array which is returned.
289      */
readFully()290     public byte[] readFully() throws IOException {
291         FileInputStream stream = openRead();
292         try {
293             int pos = 0;
294             int avail = stream.available();
295             byte[] data = new byte[avail];
296             while (true) {
297                 int amt = stream.read(data, pos, data.length-pos);
298                 //Log.i("foo", "Read " + amt + " bytes at " + pos
299                 //        + " of avail " + data.length);
300                 if (amt <= 0) {
301                     //Log.i("foo", "**** FINISHED READING: pos=" + pos
302                     //        + " len=" + data.length);
303                     return data;
304                 }
305                 pos += amt;
306                 avail = stream.available();
307                 if (avail > data.length-pos) {
308                     byte[] newData = new byte[pos+avail];
309                     System.arraycopy(data, 0, newData, 0, pos);
310                     data = newData;
311                 }
312             }
313         } finally {
314             stream.close();
315         }
316     }
317 
318     /** @hide */
write(Consumer<FileOutputStream> writeContent)319     public void write(Consumer<FileOutputStream> writeContent) {
320         FileOutputStream out = null;
321         try {
322             out = startWrite();
323             writeContent.accept(out);
324             finishWrite(out);
325         } catch (Throwable t) {
326             failWrite(out);
327             throw ExceptionUtils.propagate(t);
328         } finally {
329             IoUtils.closeQuietly(out);
330         }
331     }
332 
rename(File source, File target)333     private static void rename(File source, File target) {
334         // We used to delete the target file before rename, but that isn't atomic, and the rename()
335         // syscall should atomically replace the target file. However in the case where the target
336         // file is a directory, a simple rename() won't work. We need to delete the file in this
337         // case because there are callers who erroneously called mBaseName.mkdirs() (instead of
338         // mBaseName.getParentFile().mkdirs()) before creating the AtomicFile, and it worked
339         // regardless, so this deletion became some kind of API.
340         if (target.isDirectory()) {
341             if (!target.delete()) {
342                 Log.e(LOG_TAG, "Failed to delete file which is a directory " + target);
343             }
344         }
345         if (!source.renameTo(target)) {
346             Log.e(LOG_TAG, "Failed to rename " + source + " to " + target);
347         }
348     }
349 }
350