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.os.SystemClock;
     21 
     22 import libcore.io.IoUtils;
     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 import java.util.function.Consumer;
     30 
     31 /**
     32  * Helper class for performing atomic operations on a file by creating a
     33  * backup file until a write has successfully completed.  If you need this
     34  * on older versions of the platform you can use
     35  * {@link android.support.v4.util.AtomicFile} in the v4 support library.
     36  * <p>
     37  * Atomic file guarantees file integrity by ensuring that a file has
     38  * been completely written and sync'd to disk before removing its backup.
     39  * As long as the backup file exists, the original file is considered
     40  * to be invalid (left over from a previous attempt to write the file).
     41  * </p><p>
     42  * Atomic file does not confer any file locking semantics.
     43  * Do not use this class when the file may be accessed or modified concurrently
     44  * by multiple threads or processes.  The caller is responsible for ensuring
     45  * appropriate mutual exclusion invariants whenever it accesses the file.
     46  * </p>
     47  */
     48 public class AtomicFile {
     49     private final File mBaseName;
     50     private final File mBackupName;
     51     private final String mCommitTag;
     52     private long mStartTime;
     53 
     54     /**
     55      * Create a new AtomicFile for a file located at the given File path.
     56      * The secondary backup file will be the same file path with ".bak" appended.
     57      */
     58     public AtomicFile(File baseName) {
     59         this(baseName, null);
     60     }
     61 
     62     /**
     63      * @hide Internal constructor that also allows you to have the class
     64      * automatically log commit events.
     65      */
     66     public AtomicFile(File baseName, String commitTag) {
     67         mBaseName = baseName;
     68         mBackupName = new File(baseName.getPath() + ".bak");
     69         mCommitTag = commitTag;
     70     }
     71 
     72     /**
     73      * Return the path to the base file.  You should not generally use this,
     74      * as the data at that path may not be valid.
     75      */
     76     public File getBaseFile() {
     77         return mBaseName;
     78     }
     79 
     80     /**
     81      * Delete the atomic file.  This deletes both the base and backup files.
     82      */
     83     public void delete() {
     84         mBaseName.delete();
     85         mBackupName.delete();
     86     }
     87 
     88     /**
     89      * Start a new write operation on the file.  This returns a FileOutputStream
     90      * to which you can write the new file data.  The existing file is replaced
     91      * with the new data.  You <em>must not</em> directly close the given
     92      * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
     93      * or {@link #failWrite(FileOutputStream)}.
     94      *
     95      * <p>Note that if another thread is currently performing
     96      * a write, this will simply replace whatever that thread is writing
     97      * with the new file being written by this thread, and when the other
     98      * thread finishes the write the new write operation will no longer be
     99      * safe (or will be lost).  You must do your own threading protection for
    100      * access to AtomicFile.
    101      */
    102     public FileOutputStream startWrite() throws IOException {
    103         return startWrite(mCommitTag != null ? SystemClock.uptimeMillis() : 0);
    104     }
    105 
    106     /**
    107      * @hide Internal version of {@link #startWrite()} that allows you to specify an earlier
    108      * start time of the operation to adjust how the commit is logged.
    109      * @param startTime The effective start time of the operation, in the time
    110      * base of {@link SystemClock#uptimeMillis()}.
    111      */
    112     public FileOutputStream startWrite(long startTime) throws IOException {
    113         mStartTime = startTime;
    114 
    115         // Rename the current file so it may be used as a backup during the next read
    116         if (mBaseName.exists()) {
    117             if (!mBackupName.exists()) {
    118                 if (!mBaseName.renameTo(mBackupName)) {
    119                     Log.w("AtomicFile", "Couldn't rename file " + mBaseName
    120                             + " to backup file " + mBackupName);
    121                 }
    122             } else {
    123                 mBaseName.delete();
    124             }
    125         }
    126         FileOutputStream str = null;
    127         try {
    128             str = new FileOutputStream(mBaseName);
    129         } catch (FileNotFoundException e) {
    130             File parent = mBaseName.getParentFile();
    131             if (!parent.mkdirs()) {
    132                 throw new IOException("Couldn't create directory " + mBaseName);
    133             }
    134             FileUtils.setPermissions(
    135                 parent.getPath(),
    136                 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
    137                 -1, -1);
    138             try {
    139                 str = new FileOutputStream(mBaseName);
    140             } catch (FileNotFoundException e2) {
    141                 throw new IOException("Couldn't create " + mBaseName);
    142             }
    143         }
    144         return str;
    145     }
    146 
    147     /**
    148      * Call when you have successfully finished writing to the stream
    149      * returned by {@link #startWrite()}.  This will close, sync, and
    150      * commit the new data.  The next attempt to read the atomic file
    151      * will return the new file stream.
    152      */
    153     public void finishWrite(FileOutputStream str) {
    154         if (str != null) {
    155             FileUtils.sync(str);
    156             try {
    157                 str.close();
    158                 mBackupName.delete();
    159             } catch (IOException e) {
    160                 Log.w("AtomicFile", "finishWrite: Got exception:", e);
    161             }
    162             if (mCommitTag != null) {
    163                 com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
    164                         mCommitTag, SystemClock.uptimeMillis() - mStartTime);
    165             }
    166         }
    167     }
    168 
    169     /**
    170      * Call when you have failed for some reason at writing to the stream
    171      * returned by {@link #startWrite()}.  This will close the current
    172      * write stream, and roll back to the previous state of the file.
    173      */
    174     public void failWrite(FileOutputStream str) {
    175         if (str != null) {
    176             FileUtils.sync(str);
    177             try {
    178                 str.close();
    179                 mBaseName.delete();
    180                 mBackupName.renameTo(mBaseName);
    181             } catch (IOException e) {
    182                 Log.w("AtomicFile", "failWrite: Got exception:", e);
    183             }
    184         }
    185     }
    186 
    187     /** @hide
    188      * @deprecated This is not safe.
    189      */
    190     @Deprecated public void truncate() throws IOException {
    191         try {
    192             FileOutputStream fos = new FileOutputStream(mBaseName);
    193             FileUtils.sync(fos);
    194             fos.close();
    195         } catch (FileNotFoundException e) {
    196             throw new IOException("Couldn't append " + mBaseName);
    197         } catch (IOException e) {
    198         }
    199     }
    200 
    201     /** @hide
    202      * @deprecated This is not safe.
    203      */
    204     @Deprecated public FileOutputStream openAppend() throws IOException {
    205         try {
    206             return new FileOutputStream(mBaseName, true);
    207         } catch (FileNotFoundException e) {
    208             throw new IOException("Couldn't append " + mBaseName);
    209         }
    210     }
    211 
    212     /**
    213      * Open the atomic file for reading.  If there previously was an
    214      * incomplete write, this will roll back to the last good data before
    215      * opening for read.  You should call close() on the FileInputStream when
    216      * you are done reading from it.
    217      *
    218      * <p>Note that if another thread is currently performing
    219      * a write, this will incorrectly consider it to be in the state of a bad
    220      * write and roll back, causing the new data currently being written to
    221      * be dropped.  You must do your own threading protection for access to
    222      * AtomicFile.
    223      */
    224     public FileInputStream openRead() throws FileNotFoundException {
    225         if (mBackupName.exists()) {
    226             mBaseName.delete();
    227             mBackupName.renameTo(mBaseName);
    228         }
    229         return new FileInputStream(mBaseName);
    230     }
    231 
    232     /**
    233      * @hide
    234      * Checks if the original or backup file exists.
    235      * @return whether the original or backup file exists.
    236      */
    237     public boolean exists() {
    238         return mBaseName.exists() || mBackupName.exists();
    239     }
    240 
    241     /**
    242      * Gets the last modified time of the atomic file.
    243      * {@hide}
    244      *
    245      * @return last modified time in milliseconds since epoch.  Returns zero if
    246      *     the file does not exist or an I/O error is encountered.
    247      */
    248     public long getLastModifiedTime() {
    249         if (mBackupName.exists()) {
    250             return mBackupName.lastModified();
    251         }
    252         return mBaseName.lastModified();
    253     }
    254 
    255     /**
    256      * A convenience for {@link #openRead()} that also reads all of the
    257      * file contents into a byte array which is returned.
    258      */
    259     public byte[] readFully() throws IOException {
    260         FileInputStream stream = openRead();
    261         try {
    262             int pos = 0;
    263             int avail = stream.available();
    264             byte[] data = new byte[avail];
    265             while (true) {
    266                 int amt = stream.read(data, pos, data.length-pos);
    267                 //Log.i("foo", "Read " + amt + " bytes at " + pos
    268                 //        + " of avail " + data.length);
    269                 if (amt <= 0) {
    270                     //Log.i("foo", "**** FINISHED READING: pos=" + pos
    271                     //        + " len=" + data.length);
    272                     return data;
    273                 }
    274                 pos += amt;
    275                 avail = stream.available();
    276                 if (avail > data.length-pos) {
    277                     byte[] newData = new byte[pos+avail];
    278                     System.arraycopy(data, 0, newData, 0, pos);
    279                     data = newData;
    280                 }
    281             }
    282         } finally {
    283             stream.close();
    284         }
    285     }
    286 
    287     /** @hide */
    288     public void write(Consumer<FileOutputStream> writeContent) {
    289         FileOutputStream out = null;
    290         try {
    291             out = startWrite();
    292             writeContent.accept(out);
    293             finishWrite(out);
    294         } catch (Throwable t) {
    295             failWrite(out);
    296             throw ExceptionUtils.propagate(t);
    297         } finally {
    298             IoUtils.closeQuietly(out);
    299         }
    300     }
    301 }
    302