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