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.util.Log;
21
22import java.io.File;
23import java.io.FileInputStream;
24import java.io.FileNotFoundException;
25import java.io.FileOutputStream;
26import java.io.IOException;
27
28/**
29 * Helper class for performing atomic operations on a file by creating a
30 * backup file until a write has successfully completed.  If you need this
31 * on older versions of the platform you can use
32 * {@link android.support.v4.util.AtomicFile} in the v4 support library.
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(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    public File getBaseFile() {
63        return mBaseName;
64    }
65
66    /**
67     * Delete the atomic file.  This deletes both the base and backup files.
68     */
69    public void delete() {
70        mBaseName.delete();
71        mBackupName.delete();
72    }
73
74    /**
75     * Start a new write operation on the file.  This returns a FileOutputStream
76     * to which you can write the new file data.  The existing file is replaced
77     * with the new data.  You <em>must not</em> directly close the given
78     * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
79     * or {@link #failWrite(FileOutputStream)}.
80     *
81     * <p>Note that if another thread is currently performing
82     * a write, this will simply replace whatever that thread is writing
83     * with the new file being written by this thread, and when the other
84     * thread finishes the write the new write operation will no longer be
85     * safe (or will be lost).  You must do your own threading protection for
86     * access to AtomicFile.
87     */
88    public FileOutputStream startWrite() throws IOException {
89        // Rename the current file so it may be used as a backup during the next read
90        if (mBaseName.exists()) {
91            if (!mBackupName.exists()) {
92                if (!mBaseName.renameTo(mBackupName)) {
93                    Log.w("AtomicFile", "Couldn't rename file " + mBaseName
94                            + " to backup file " + mBackupName);
95                }
96            } else {
97                mBaseName.delete();
98            }
99        }
100        FileOutputStream str = null;
101        try {
102            str = new FileOutputStream(mBaseName);
103        } catch (FileNotFoundException e) {
104            File parent = mBaseName.getParentFile();
105            if (!parent.mkdirs()) {
106                throw new IOException("Couldn't create directory " + mBaseName);
107            }
108            FileUtils.setPermissions(
109                parent.getPath(),
110                FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
111                -1, -1);
112            try {
113                str = new FileOutputStream(mBaseName);
114            } catch (FileNotFoundException e2) {
115                throw new IOException("Couldn't create " + mBaseName);
116            }
117        }
118        return str;
119    }
120
121    /**
122     * Call when you have successfully finished writing to the stream
123     * returned by {@link #startWrite()}.  This will close, sync, and
124     * commit the new data.  The next attempt to read the atomic file
125     * will return the new file stream.
126     */
127    public void finishWrite(FileOutputStream str) {
128        if (str != null) {
129            FileUtils.sync(str);
130            try {
131                str.close();
132                mBackupName.delete();
133            } catch (IOException e) {
134                Log.w("AtomicFile", "finishWrite: Got exception:", e);
135            }
136        }
137    }
138
139    /**
140     * Call when you have failed for some reason at writing to the stream
141     * returned by {@link #startWrite()}.  This will close the current
142     * write stream, and roll back to the previous state of the file.
143     */
144    public void failWrite(FileOutputStream str) {
145        if (str != null) {
146            FileUtils.sync(str);
147            try {
148                str.close();
149                mBaseName.delete();
150                mBackupName.renameTo(mBaseName);
151            } catch (IOException e) {
152                Log.w("AtomicFile", "failWrite: Got exception:", e);
153            }
154        }
155    }
156
157    /** @hide
158     * @deprecated This is not safe.
159     */
160    @Deprecated public void truncate() throws IOException {
161        try {
162            FileOutputStream fos = new FileOutputStream(mBaseName);
163            FileUtils.sync(fos);
164            fos.close();
165        } catch (FileNotFoundException e) {
166            throw new IOException("Couldn't append " + mBaseName);
167        } catch (IOException e) {
168        }
169    }
170
171    /** @hide
172     * @deprecated This is not safe.
173     */
174    @Deprecated public FileOutputStream openAppend() throws IOException {
175        try {
176            return new FileOutputStream(mBaseName, true);
177        } catch (FileNotFoundException e) {
178            throw new IOException("Couldn't append " + mBaseName);
179        }
180    }
181
182    /**
183     * Open the atomic file for reading.  If there previously was an
184     * incomplete write, this will roll back to the last good data before
185     * opening for read.  You should call close() on the FileInputStream when
186     * you are done reading from it.
187     *
188     * <p>Note that if another thread is currently performing
189     * a write, this will incorrectly consider it to be in the state of a bad
190     * write and roll back, causing the new data currently being written to
191     * be dropped.  You must do your own threading protection for access to
192     * AtomicFile.
193     */
194    public FileInputStream openRead() throws FileNotFoundException {
195        if (mBackupName.exists()) {
196            mBaseName.delete();
197            mBackupName.renameTo(mBaseName);
198        }
199        return new FileInputStream(mBaseName);
200    }
201
202    /**
203     * Gets the last modified time of the atomic file.
204     * {@hide}
205     *
206     * @return last modified time in milliseconds since epoch.
207     * @throws IOException
208     */
209    public long getLastModifiedTime() throws IOException {
210        if (mBackupName.exists()) {
211            return mBackupName.lastModified();
212        }
213        return mBaseName.lastModified();
214    }
215
216    /**
217     * A convenience for {@link #openRead()} that also reads all of the
218     * file contents into a byte array which is returned.
219     */
220    public byte[] readFully() throws IOException {
221        FileInputStream stream = openRead();
222        try {
223            int pos = 0;
224            int avail = stream.available();
225            byte[] data = new byte[avail];
226            while (true) {
227                int amt = stream.read(data, pos, data.length-pos);
228                //Log.i("foo", "Read " + amt + " bytes at " + pos
229                //        + " of avail " + data.length);
230                if (amt <= 0) {
231                    //Log.i("foo", "**** FINISHED READING: pos=" + pos
232                    //        + " len=" + data.length);
233                    return data;
234                }
235                pos += amt;
236                avail = stream.available();
237                if (avail > data.length-pos) {
238                    byte[] newData = new byte[pos+avail];
239                    System.arraycopy(data, 0, newData, 0, pos);
240                    data = newData;
241                }
242            }
243        } finally {
244            stream.close();
245        }
246    }
247}
248