blob: 4fca570f6614ad485bf1dbcd5ca37afc6c417f29 [file] [log] [blame]
Dianne Hackborn39606a02012-07-31 17:54:35 -07001/*
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
17package android.util;
18
19import android.os.FileUtils;
20import android.util.Log;
21
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileNotFoundException;
25import java.io.FileOutputStream;
26import 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 */
45public 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 */
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 */
62 public File getBaseFile() {
63 return mBaseName;
64 }
65
66 /**
67 * Delete the atomic file. This deletes both the base and backup files.
68 */
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 */
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.mkdir()) {
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 */
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 */
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 */
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 */
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 */
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 * A convenience for {@link #openRead()} that also reads all of the
204 * file contents into a byte array which is returned.
205 */
206 public byte[] readFully() throws IOException {
207 FileInputStream stream = openRead();
208 try {
209 int pos = 0;
210 int avail = stream.available();
211 byte[] data = new byte[avail];
212 while (true) {
213 int amt = stream.read(data, pos, data.length-pos);
214 //Log.i("foo", "Read " + amt + " bytes at " + pos
215 // + " of avail " + data.length);
216 if (amt <= 0) {
217 //Log.i("foo", "**** FINISHED READING: pos=" + pos
218 // + " len=" + data.length);
219 return data;
220 }
221 pos += amt;
222 avail = stream.available();
223 if (avail > data.length-pos) {
224 byte[] newData = new byte[pos+avail];
225 System.arraycopy(data, 0, newData, 0, pos);
226 data = newData;
227 }
228 }
229 } finally {
230 stream.close();
231 }
232 }
233}