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;
20
21import libcore.io.IoUtils;
22
23import java.io.File;
24import java.io.FileInputStream;
25import java.io.FileNotFoundException;
26import java.io.FileOutputStream;
27import java.io.IOException;
28import 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 */
47public 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     */
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     */
64    public File getBaseFile() {
65        return mBaseName;
66    }
67
68    /**
69     * Delete the atomic file.  This deletes both the base and backup files.
70     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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 */
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