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