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 
     21 import libcore.io.IoUtils;
     22 
     23 import java.io.File;
     24 import java.io.FileInputStream;
     25 import java.io.FileNotFoundException;
     26 import java.io.FileOutputStream;
     27 import java.io.IOException;
     28 import java.util.function.Consumer;
     29 
     30 /**
     31  * Helper class for performing atomic operations on a file by creating a
     32  * backup file until a write has successfully completed.  If you need this
     33  * on older versions of the platform you can use
     34  * {@link android.support.v4.util.AtomicFile} in the v4 support library.
     35  * <p>
     36  * Atomic file guarantees file integrity by ensuring that a file has
     37  * been completely written and sync'd to disk before removing its backup.
     38  * As long as the backup file exists, the original file is considered
     39  * to be invalid (left over from a previous attempt to write the file).
     40  * </p><p>
     41  * Atomic file does not confer any file locking semantics.
     42  * Do not use this class when the file may be accessed or modified concurrently
     43  * by multiple threads or processes.  The caller is responsible for ensuring
     44  * appropriate mutual exclusion invariants whenever it accesses the file.
     45  * </p>
     46  */
     47 public class AtomicFile {
     48     private final File mBaseName;
     49     private final File mBackupName;
     50 
     51     /**
     52      * Create a new AtomicFile for a file located at the given File path.
     53      * The secondary backup file will be the same file path with ".bak" appended.
     54      */
     55     public AtomicFile(File baseName) {
     56         mBaseName = baseName;
     57         mBackupName = new File(baseName.getPath() + ".bak");
     58     }
     59 
     60     /**
     61      * Return the path to the base file.  You should not generally use this,
     62      * as the data at that path may not be valid.
     63      */
     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     public FileOutputStream startWrite() throws IOException {
     91         // Rename the current file so it may be used as a backup during the next read
     92         if (mBaseName.exists()) {
     93             if (!mBackupName.exists()) {
     94                 if (!mBaseName.renameTo(mBackupName)) {
     95                     Log.w("AtomicFile", "Couldn't rename file " + mBaseName
     96                             + " to backup file " + mBackupName);
     97                 }
     98             } else {
     99                 mBaseName.delete();
    100             }
    101         }
    102         FileOutputStream str = null;
    103         try {
    104             str = new FileOutputStream(mBaseName);
    105         } catch (FileNotFoundException e) {
    106             File parent = mBaseName.getParentFile();
    107             if (!parent.mkdirs()) {
    108                 throw new IOException("Couldn't create directory " + mBaseName);
    109             }
    110             FileUtils.setPermissions(
    111                 parent.getPath(),
    112                 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
    113                 -1, -1);
    114             try {
    115                 str = new FileOutputStream(mBaseName);
    116             } catch (FileNotFoundException e2) {
    117                 throw new IOException("Couldn't create " + mBaseName);
    118             }
    119         }
    120         return str;
    121     }
    122 
    123     /**
    124      * Call when you have successfully finished writing to the stream
    125      * returned by {@link #startWrite()}.  This will close, sync, and
    126      * commit the new data.  The next attempt to read the atomic file
    127      * will return the new file stream.
    128      */
    129     public void finishWrite(FileOutputStream str) {
    130         if (str != null) {
    131             FileUtils.sync(str);
    132             try {
    133                 str.close();
    134                 mBackupName.delete();
    135             } catch (IOException e) {
    136                 Log.w("AtomicFile", "finishWrite: Got exception:", e);
    137             }
    138         }
    139     }
    140 
    141     /**
    142      * Call when you have failed for some reason at writing to the stream
    143      * returned by {@link #startWrite()}.  This will close the current
    144      * write stream, and roll back to the previous state of the file.
    145      */
    146     public void failWrite(FileOutputStream str) {
    147         if (str != null) {
    148             FileUtils.sync(str);
    149             try {
    150                 str.close();
    151                 mBaseName.delete();
    152                 mBackupName.renameTo(mBaseName);
    153             } catch (IOException e) {
    154                 Log.w("AtomicFile", "failWrite: Got exception:", e);
    155             }
    156         }
    157     }
    158 
    159     /** @hide
    160      * @deprecated This is not safe.
    161      */
    162     @Deprecated public void truncate() throws IOException {
    163         try {
    164             FileOutputStream fos = new FileOutputStream(mBaseName);
    165             FileUtils.sync(fos);
    166             fos.close();
    167         } catch (FileNotFoundException e) {
    168             throw new IOException("Couldn't append " + mBaseName);
    169         } catch (IOException e) {
    170         }
    171     }
    172 
    173     /** @hide
    174      * @deprecated This is not safe.
    175      */
    176     @Deprecated public FileOutputStream openAppend() throws IOException {
    177         try {
    178             return new FileOutputStream(mBaseName, true);
    179         } catch (FileNotFoundException e) {
    180             throw new IOException("Couldn't append " + mBaseName);
    181         }
    182     }
    183 
    184     /**
    185      * Open the atomic file for reading.  If there previously was an
    186      * incomplete write, this will roll back to the last good data before
    187      * opening for read.  You should call close() on the FileInputStream when
    188      * you are done reading from it.
    189      *
    190      * <p>Note that if another thread is currently performing
    191      * a write, this will incorrectly consider it to be in the state of a bad
    192      * write and roll back, causing the new data currently being written to
    193      * be dropped.  You must do your own threading protection for access to
    194      * AtomicFile.
    195      */
    196     public FileInputStream openRead() throws FileNotFoundException {
    197         if (mBackupName.exists()) {
    198             mBaseName.delete();
    199             mBackupName.renameTo(mBaseName);
    200         }
    201         return new FileInputStream(mBaseName);
    202     }
    203 
    204     /**
    205      * Gets the last modified time of the atomic file.
    206      * {@hide}
    207      *
    208      * @return last modified time in milliseconds since epoch.
    209      * @throws IOException
    210      */
    211     public long getLastModifiedTime() throws IOException {
    212         if (mBackupName.exists()) {
    213             return mBackupName.lastModified();
    214         }
    215         return mBaseName.lastModified();
    216     }
    217 
    218     /**
    219      * A convenience for {@link #openRead()} that also reads all of the
    220      * file contents into a byte array which is returned.
    221      */
    222     public byte[] readFully() throws IOException {
    223         FileInputStream stream = openRead();
    224         try {
    225             int pos = 0;
    226             int avail = stream.available();
    227             byte[] data = new byte[avail];
    228             while (true) {
    229                 int amt = stream.read(data, pos, data.length-pos);
    230                 //Log.i("foo", "Read " + amt + " bytes at " + pos
    231                 //        + " of avail " + data.length);
    232                 if (amt <= 0) {
    233                     //Log.i("foo", "**** FINISHED READING: pos=" + pos
    234                     //        + " len=" + data.length);
    235                     return data;
    236                 }
    237                 pos += amt;
    238                 avail = stream.available();
    239                 if (avail > data.length-pos) {
    240                     byte[] newData = new byte[pos+avail];
    241                     System.arraycopy(data, 0, newData, 0, pos);
    242                     data = newData;
    243                 }
    244             }
    245         } finally {
    246             stream.close();
    247         }
    248     }
    249 
    250     /** @hide */
    251     public void write(Consumer<FileOutputStream> writeContent) {
    252         FileOutputStream out = null;
    253         try {
    254             out = startWrite();
    255             writeContent.accept(out);
    256             finishWrite(out);
    257         } catch (Throwable t) {
    258             failWrite(out);
    259             throw ExceptionUtils.propagate(t);
    260         } finally {
    261             IoUtils.closeQuietly(out);
    262         }
    263     }
    264 }
    265