blob: cf7ed9b0566dd22dc14314e206861f4e1f5ed059 [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;
Dianne Hackborne17b4452018-01-10 13:15:40 -080020import android.os.SystemClock;
Eugene Susla47aafbe2017-02-13 12:46:46 -080021
22import libcore.io.IoUtils;
Dianne Hackborn39606a02012-07-31 17:54:35 -070023
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
28import java.io.IOException;
Eugene Susla47aafbe2017-02-13 12:46:46 -080029import java.util.function.Consumer;
Dianne Hackborn39606a02012-07-31 17:54:35 -070030
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 */
48public class AtomicFile {
49 private final File mBaseName;
50 private final File mBackupName;
Dianne Hackborne17b4452018-01-10 13:15:40 -080051 private final String mCommitTag;
52 private long mStartTime;
Dianne Hackborn39606a02012-07-31 17:54:35 -070053
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 */
58 public AtomicFile(File baseName) {
Dianne Hackborne17b4452018-01-10 13:15:40 -080059 this(baseName, null);
60 }
61
62 /**
63 * @hide Internal constructor that also allows you to have the class
64 * automatically log commit events.
65 */
66 public AtomicFile(File baseName, String commitTag) {
Dianne Hackborn39606a02012-07-31 17:54:35 -070067 mBaseName = baseName;
68 mBackupName = new File(baseName.getPath() + ".bak");
Dianne Hackborne17b4452018-01-10 13:15:40 -080069 mCommitTag = commitTag;
Dianne Hackborn39606a02012-07-31 17:54:35 -070070 }
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 */
76 public File getBaseFile() {
77 return mBaseName;
78 }
79
80 /**
81 * Delete the atomic file. This deletes both the base and backup files.
82 */
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 */
102 public FileOutputStream startWrite() throws IOException {
Dianne Hackborne17b4452018-01-10 13:15:40 -0800103 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 */
112 public FileOutputStream startWrite(long startTime) throws IOException {
113 mStartTime = startTime;
114
Dianne Hackborn39606a02012-07-31 17:54:35 -0700115 // 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();
Svetoslav683914b2015-01-15 14:22:26 -0800131 if (!parent.mkdirs()) {
Dianne Hackborn39606a02012-07-31 17:54:35 -0700132 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 */
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 }
Dianne Hackborne17b4452018-01-10 13:15:40 -0800162 if (mCommitTag != null) {
163 com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
164 mCommitTag, SystemClock.uptimeMillis() - mStartTime);
165 }
Dianne Hackborn39606a02012-07-31 17:54:35 -0700166 }
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 */
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 */
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 */
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 */
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 /**
Amith Yamasani74bf71b2017-07-19 16:49:52 -0700233 * @hide
234 * Checks if the original or backup file exists.
235 * @return whether the original or backup file exists.
236 */
237 public boolean exists() {
238 return mBaseName.exists() || mBackupName.exists();
239 }
240
241 /**
Adam Lesinski0debc9a2014-07-16 19:09:13 -0700242 * Gets the last modified time of the atomic file.
243 * {@hide}
244 *
Christopher Tate616541d2017-07-26 14:27:38 -0700245 * @return last modified time in milliseconds since epoch. Returns zero if
246 * the file does not exist or an I/O error is encountered.
Adam Lesinski0debc9a2014-07-16 19:09:13 -0700247 */
Christopher Tate616541d2017-07-26 14:27:38 -0700248 public long getLastModifiedTime() {
Adam Lesinski0debc9a2014-07-16 19:09:13 -0700249 if (mBackupName.exists()) {
250 return mBackupName.lastModified();
251 }
252 return mBaseName.lastModified();
253 }
254
255 /**
Dianne Hackborn39606a02012-07-31 17:54:35 -0700256 * A convenience for {@link #openRead()} that also reads all of the
257 * file contents into a byte array which is returned.
258 */
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 }
Eugene Susla47aafbe2017-02-13 12:46:46 -0800286
287 /** @hide */
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 }
Dianne Hackborn39606a02012-07-31 17:54:35 -0700301}