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
17package android.util;
18
19import android.os.FileUtils;
20import android.os.SystemClock;
21
22import libcore.io.IoUtils;
23
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
28import java.io.IOException;
29import 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 */
48public 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     */
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     */
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     */
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 {
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     */
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     */
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     */
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    /**
233     * @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    /**
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     */
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     */
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 */
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