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