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