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