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 android.util.Log;
20
21import androidx.annotation.NonNull;
22import androidx.annotation.Nullable;
23
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
28import java.io.IOException;
29
30/**
31 * Static library support version of the framework's {@link android.util.AtomicFile},
32 * a helper class for performing atomic operations on a file by creating a
33 * backup file until a write has successfully completed.
34 * <p>
35 * Atomic file guarantees file integrity by ensuring that a file has
36 * been completely written and sync'd to disk before removing its backup.
37 * As long as the backup file exists, the original file is considered
38 * to be invalid (left over from a previous attempt to write the file).
39 * </p><p>
40 * Atomic file does not confer any file locking semantics.
41 * Do not use this class when the file may be accessed or modified concurrently
42 * by multiple threads or processes.  The caller is responsible for ensuring
43 * appropriate mutual exclusion invariants whenever it accesses the file.
44 * </p>
45 */
46public class AtomicFile {
47    private final File mBaseName;
48    private final File mBackupName;
49
50    /**
51     * Create a new AtomicFile for a file located at the given File path.
52     * The secondary backup file will be the same file path with ".bak" appended.
53     */
54    public AtomicFile(@NonNull File baseName) {
55        mBaseName = baseName;
56        mBackupName = new File(baseName.getPath() + ".bak");
57    }
58
59    /**
60     * Return the path to the base file.  You should not generally use this,
61     * as the data at that path may not be valid.
62     */
63    @NonNull
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    @NonNull
91    public FileOutputStream startWrite() throws IOException {
92        // Rename the current file so it may be used as a backup during the next read
93        if (mBaseName.exists()) {
94            if (!mBackupName.exists()) {
95                if (!mBaseName.renameTo(mBackupName)) {
96                    Log.w("AtomicFile", "Couldn't rename file " + mBaseName
97                            + " to backup file " + mBackupName);
98                }
99            } else {
100                mBaseName.delete();
101            }
102        }
103        FileOutputStream str;
104        try {
105            str = new FileOutputStream(mBaseName);
106        } catch (FileNotFoundException e) {
107            File parent = mBaseName.getParentFile();
108            if (!parent.mkdirs()) {
109                throw new IOException("Couldn't create directory " + mBaseName);
110            }
111            try {
112                str = new FileOutputStream(mBaseName);
113            } catch (FileNotFoundException e2) {
114                throw new IOException("Couldn't create " + mBaseName);
115            }
116        }
117        return str;
118    }
119
120    /**
121     * Call when you have successfully finished writing to the stream
122     * returned by {@link #startWrite()}.  This will close, sync, and
123     * commit the new data.  The next attempt to read the atomic file
124     * will return the new file stream.
125     */
126    public void finishWrite(@Nullable FileOutputStream str) {
127        if (str != null) {
128            sync(str);
129            try {
130                str.close();
131                mBackupName.delete();
132            } catch (IOException e) {
133                Log.w("AtomicFile", "finishWrite: Got exception:", e);
134            }
135        }
136    }
137
138    /**
139     * Call when you have failed for some reason at writing to the stream
140     * returned by {@link #startWrite()}.  This will close the current
141     * write stream, and roll back to the previous state of the file.
142     */
143    public void failWrite(@Nullable FileOutputStream str) {
144        if (str != null) {
145            sync(str);
146            try {
147                str.close();
148                mBaseName.delete();
149                mBackupName.renameTo(mBaseName);
150            } catch (IOException e) {
151                Log.w("AtomicFile", "failWrite: Got exception:", e);
152            }
153        }
154    }
155
156    /**
157     * Open the atomic file for reading.  If there previously was an
158     * incomplete write, this will roll back to the last good data before
159     * opening for read.  You should call close() on the FileInputStream when
160     * you are done reading from it.
161     *
162     * <p>Note that if another thread is currently performing
163     * a write, this will incorrectly consider it to be in the state of a bad
164     * write and roll back, causing the new data currently being written to
165     * be dropped.  You must do your own threading protection for access to
166     * AtomicFile.
167     */
168    @NonNull
169    public FileInputStream openRead() throws FileNotFoundException {
170        if (mBackupName.exists()) {
171            mBaseName.delete();
172            mBackupName.renameTo(mBaseName);
173        }
174        return new FileInputStream(mBaseName);
175    }
176
177    /**
178     * A convenience for {@link #openRead()} that also reads all of the
179     * file contents into a byte array which is returned.
180     */
181    @NonNull
182    public byte[] readFully() throws IOException {
183        FileInputStream stream = openRead();
184        try {
185            int pos = 0;
186            int avail = stream.available();
187            byte[] data = new byte[avail];
188            while (true) {
189                int amt = stream.read(data, pos, data.length-pos);
190                //Log.i("foo", "Read " + amt + " bytes at " + pos
191                //        + " of avail " + data.length);
192                if (amt <= 0) {
193                    //Log.i("foo", "**** FINISHED READING: pos=" + pos
194                    //        + " len=" + data.length);
195                    return data;
196                }
197                pos += amt;
198                avail = stream.available();
199                if (avail > data.length-pos) {
200                    byte[] newData = new byte[pos+avail];
201                    System.arraycopy(data, 0, newData, 0, pos);
202                    data = newData;
203                }
204            }
205        } finally {
206            stream.close();
207        }
208    }
209
210    private static boolean sync(@NonNull FileOutputStream stream) {
211        try {
212            stream.getFD().sync();
213            return true;
214        } catch (IOException e) {
215        }
216        return false;
217    }
218}
219