Home | History | Annotate | Download | only in util
      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 
     17 package android.util;
     18 
     19 import android.os.FileUtils;
     20 import android.util.Log;
     21 
     22 import java.io.File;
     23 import java.io.FileInputStream;
     24 import java.io.FileNotFoundException;
     25 import java.io.FileOutputStream;
     26 import 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  */
     45 public 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