AtomicFile.java revision b31c3281d870e9abb673db239234d580dcc4feff
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 androidx.core.util;
18
19import androidx.annotation.NonNull;
20import androidx.annotation.Nullable;
21import android.util.Log;
22
23import java.io.File;
24import java.io.FileInputStream;
25import java.io.FileNotFoundException;
26import java.io.FileOutputStream;
27import java.io.IOException;
28
29/**
30 * Static library support version of the framework's {@link android.util.AtomicFile},
31 * a helper class for performing atomic operations on a file by creating a
32 * backup file until a write has successfully completed.
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(@NonNull 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    @NonNull
63    public File getBaseFile() {
64        return mBaseName;
65    }
66
67    /**
68     * Delete the atomic file.  This deletes both the base and backup files.
69     */
70    public void delete() {
71        mBaseName.delete();
72        mBackupName.delete();
73    }
74
75    /**
76     * Start a new write operation on the file.  This returns a FileOutputStream
77     * to which you can write the new file data.  The existing file is replaced
78     * with the new data.  You <em>must not</em> directly close the given
79     * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
80     * or {@link #failWrite(FileOutputStream)}.
81     *
82     * <p>Note that if another thread is currently performing
83     * a write, this will simply replace whatever that thread is writing
84     * with the new file being written by this thread, and when the other
85     * thread finishes the write the new write operation will no longer be
86     * safe (or will be lost).  You must do your own threading protection for
87     * access to AtomicFile.
88     */
89    @NonNull
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;
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            try {
111                str = new FileOutputStream(mBaseName);
112            } catch (FileNotFoundException e2) {
113                throw new IOException("Couldn't create " + mBaseName);
114            }
115        }
116        return str;
117    }
118
119    /**
120     * Call when you have successfully finished writing to the stream
121     * returned by {@link #startWrite()}.  This will close, sync, and
122     * commit the new data.  The next attempt to read the atomic file
123     * will return the new file stream.
124     */
125    public void finishWrite(@Nullable FileOutputStream str) {
126        if (str != null) {
127            sync(str);
128            try {
129                str.close();
130                mBackupName.delete();
131            } catch (IOException e) {
132                Log.w("AtomicFile", "finishWrite: Got exception:", e);
133            }
134        }
135    }
136
137    /**
138     * Call when you have failed for some reason at writing to the stream
139     * returned by {@link #startWrite()}.  This will close the current
140     * write stream, and roll back to the previous state of the file.
141     */
142    public void failWrite(@Nullable FileOutputStream str) {
143        if (str != null) {
144            sync(str);
145            try {
146                str.close();
147                mBaseName.delete();
148                mBackupName.renameTo(mBaseName);
149            } catch (IOException e) {
150                Log.w("AtomicFile", "failWrite: Got exception:", e);
151            }
152        }
153    }
154
155    /**
156     * Open the atomic file for reading.  If there previously was an
157     * incomplete write, this will roll back to the last good data before
158     * opening for read.  You should call close() on the FileInputStream when
159     * you are done reading from it.
160     *
161     * <p>Note that if another thread is currently performing
162     * a write, this will incorrectly consider it to be in the state of a bad
163     * write and roll back, causing the new data currently being written to
164     * be dropped.  You must do your own threading protection for access to
165     * AtomicFile.
166     */
167    @NonNull
168    public FileInputStream openRead() throws FileNotFoundException {
169        if (mBackupName.exists()) {
170            mBaseName.delete();
171            mBackupName.renameTo(mBaseName);
172        }
173        return new FileInputStream(mBaseName);
174    }
175
176    /**
177     * A convenience for {@link #openRead()} that also reads all of the
178     * file contents into a byte array which is returned.
179     */
180    @NonNull
181    public byte[] readFully() throws IOException {
182        FileInputStream stream = openRead();
183        try {
184            int pos = 0;
185            int avail = stream.available();
186            byte[] data = new byte[avail];
187            while (true) {
188                int amt = stream.read(data, pos, data.length-pos);
189                //Log.i("foo", "Read " + amt + " bytes at " + pos
190                //        + " of avail " + data.length);
191                if (amt <= 0) {
192                    //Log.i("foo", "**** FINISHED READING: pos=" + pos
193                    //        + " len=" + data.length);
194                    return data;
195                }
196                pos += amt;
197                avail = stream.available();
198                if (avail > data.length-pos) {
199                    byte[] newData = new byte[pos+avail];
200                    System.arraycopy(data, 0, newData, 0, pos);
201                    data = newData;
202                }
203            }
204        } finally {
205            stream.close();
206        }
207    }
208
209    private static boolean sync(@NonNull FileOutputStream stream) {
210        try {
211            stream.getFD().sync();
212            return true;
213        } catch (IOException e) {
214        }
215        return false;
216    }
217}
218