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 androidx.core.util;
     18 
     19 import android.util.Log;
     20 
     21 import androidx.annotation.NonNull;
     22 import androidx.annotation.Nullable;
     23 
     24 import java.io.File;
     25 import java.io.FileInputStream;
     26 import java.io.FileNotFoundException;
     27 import java.io.FileOutputStream;
     28 import 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  */
     46 public 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