content: /*
   * Copyright (C) 2009 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.util;
  
  import android.os.FileUtils;
  import android.os.SystemClock;
  
  import libcore.io.IoUtils;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileNotFoundException;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.function.Consumer;
  
  /**
   * Helper class for performing atomic operations on a file by creating a
   * backup file until a write has successfully completed.  If you need this
   * on older versions of the platform you can use
   * {@link android.support.v4.util.AtomicFile} in the v4 support library.
   * <p>
   * Atomic file guarantees file integrity by ensuring that a file has
   * been completely written and sync'd to disk before removing its backup.
   * As long as the backup file exists, the original file is considered
   * to be invalid (left over from a previous attempt to write the file).
   * </p><p>
   * Atomic file does not confer any file locking semantics.
   * Do not use this class when the file may be accessed or modified concurrently
   * by multiple threads or processes.  The caller is responsible for ensuring
   * appropriate mutual exclusion invariants whenever it accesses the file.
   * </p>
   */
  public class AtomicFile {
      private final File mBaseName;
      private final File mBackupName;
      private final String mCommitTag;
      private long mStartTime;
  
      /**
       * Create a new AtomicFile for a file located at the given File path.
       * The secondary backup file will be the same file path with ".bak" appended.
       */
      public AtomicFile(File baseName) {
          this(baseName, null);
      }
  
      /**
       * @hide Internal constructor that also allows you to have the class
       * automatically log commit events.
       */
      public AtomicFile(File baseName, String commitTag) {
          mBaseName = baseName;
          mBackupName = new File(baseName.getPath() + ".bak");
          mCommitTag = commitTag;
      }
  
      /**
       * Return the path to the base file.  You should not generally use this,
       * as the data at that path may not be valid.
       */
      public File getBaseFile() {
          return mBaseName;
      }
  
      /**
       * Delete the atomic file.  This deletes both the base and backup files.
       */
      public void delete() {
          mBaseName.delete();
          mBackupName.delete();
      }
  
      /**
       * Start a new write operation on the file.  This returns a FileOutputStream
       * to which you can write the new file data.  The existing file is replaced
       * with the new data.  You <em>must not</em> directly close the given
       * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
       * or {@link #failWrite(FileOutputStream)}.
       *
       * <p>Note that if another thread is currently performing
       * a write, this will simply replace whatever that thread is writing
       * with the new file being written by this thread, and when the other
       * thread finishes the write the new write operation will no longer be
       * safe (or will be lost).  You must do your own threading protection for
       * access to AtomicFile.
       */
      public FileOutputStream startWrite() throws IOException {
          return startWrite(mCommitTag != null ? SystemClock.uptimeMillis() : 0);
      }
  
      /**
       * @hide Internal version of {@link #startWrite()} that allows you to specify an earlier
       * start time of the operation to adjust how the commit is logged.
       * @param startTime The effective start time of the operation, in the time
       * base of {@link SystemClock#uptimeMillis()}.
       */
      public FileOutputStream startWrite(long startTime) throws IOException {
          mStartTime = startTime;
  
          // Rename the current file so it may be used as a backup during the next read
          if (mBaseName.exists()) {
              if (!mBackupName.exists()) {
                  if (!mBaseName.renameTo(mBackupName)) {
                      Log.w("AtomicFile", "Couldn't rename file " + mBaseName
                              + " to backup file " + mBackupName);
                  }
              } else {
                  mBaseName.delete();
              }
          }
          FileOutputStream str = null;
          try {
              str = new FileOutputStream(mBaseName);
          } catch (FileNotFoundException e) {
              File parent = mBaseName.getParentFile();
              if (!parent.mkdirs()) {
                  throw new IOException("Couldn't create directory " + mBaseName);
              }
              FileUtils.setPermissions(
                  parent.getPath(),
                  FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
                  -1, -1);
              try {
                  str = new FileOutputStream(mBaseName);
              } catch (FileNotFoundException e2) {
                  throw new IOException("Couldn't create " + mBaseName);
              }
          }
          return str;
      }
  
      /**
       * Call when you have successfully finished writing to the stream
       * returned by {@link #startWrite()}.  This will close, sync, and
       * commit the new data.  The next attempt to read the atomic file
       * will return the new file stream.
       */
      public void finishWrite(FileOutputStream str) {
          if (str != null) {
              FileUtils.sync(str);
              try {
                  str.close();
                  mBackupName.delete();
              } catch (IOException e) {
                  Log.w("AtomicFile", "finishWrite: Got exception:", e);
              }
              if (mCommitTag != null) {
                  com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
                          mCommitTag, SystemClock.uptimeMillis() - mStartTime);
              }
          }
      }
  
      /**
       * Call when you have failed for some reason at writing to the stream
       * returned by {@link #startWrite()}.  This will close the current
       * write stream, and roll back to the previous state of the file.
       */
      public void failWrite(FileOutputStream str) {
          if (str != null) {
              FileUtils.sync(str);
              try {
                  str.close();
                  mBaseName.delete();
                  mBackupName.renameTo(mBaseName);
              } catch (IOException e) {
                  Log.w("AtomicFile", "failWrite: Got exception:", e);
              }
          }
      }
  
      /** @hide
       * @deprecated This is not safe.
       */
      @Deprecated public void truncate() throws IOException {
          try {
              FileOutputStream fos = new FileOutputStream(mBaseName);
              FileUtils.sync(fos);
              fos.close();
          } catch (FileNotFoundException e) {
              throw new IOException("Couldn't append " + mBaseName);
          } catch (IOException e) {
          }
      }
  
      /** @hide
       * @deprecated This is not safe.
       */
      @Deprecated public FileOutputStream openAppend() throws IOException {
          try {
              return new FileOutputStream(mBaseName, true);
          } catch (FileNotFoundException e) {
              throw new IOException("Couldn't append " + mBaseName);
          }
      }
  
      /**
       * Open the atomic file for reading.  If there previously was an
       * incomplete write, this will roll back to the last good data before
       * opening for read.  You should call close() on the FileInputStream when
       * you are done reading from it.
       *
       * <p>Note that if another thread is currently performing
       * a write, this will incorrectly consider it to be in the state of a bad
       * write and roll back, causing the new data currently being written to
       * be dropped.  You must do your own threading protection for access to
       * AtomicFile.
       */
      public FileInputStream openRead() throws FileNotFoundException {
          if (mBackupName.exists()) {
              mBaseName.delete();
              mBackupName.renameTo(mBaseName);
          }
          return new FileInputStream(mBaseName);
      }
  
      /**
       * @hide
       * Checks if the original or backup file exists.
       * @return whether the original or backup file exists.
       */
      public boolean exists() {
          return mBaseName.exists() || mBackupName.exists();
      }
  
      /**
       * Gets the last modified time of the atomic file.
       * {@hide}
       *
       * @return last modified time in milliseconds since epoch.  Returns zero if
       *     the file does not exist or an I/O error is encountered.
       */
      public long getLastModifiedTime() {
          if (mBackupName.exists()) {
              return mBackupName.lastModified();
          }
          return mBaseName.lastModified();
      }
  
      /**
       * A convenience for {@link #openRead()} that also reads all of the
       * file contents into a byte array which is returned.
       */
      public byte[] readFully() throws IOException {
          FileInputStream stream = openRead();
          try {
              int pos = 0;
              int avail = stream.available();
              byte[] data = new byte[avail];
              while (true) {
                  int amt = stream.read(data, pos, data.length-pos);
                  //Log.i("foo", "Read " + amt + " bytes at " + pos
                  //        + " of avail " + data.length);
                  if (amt <= 0) {
                      //Log.i("foo", "**** FINISHED READING: pos=" + pos
                      //        + " len=" + data.length);
                      return data;
                  }
                  pos += amt;
                  avail = stream.available();
                  if (avail > data.length-pos) {
                      byte[] newData = new byte[pos+avail];
                      System.arraycopy(data, 0, newData, 0, pos);
                      data = newData;
                  }
              }
          } finally {
              stream.close();
          }
      }
  
      /** @hide */
      public void write(Consumer<FileOutputStream> writeContent) {
          FileOutputStream out = null;
          try {
              out = startWrite();
              writeContent.accept(out);
              finishWrite(out);
          } catch (Throwable t) {
              failWrite(out);
              throw ExceptionUtils.propagate(t);
          } finally {
              IoUtils.closeQuietly(out);
          }
      }
  }
  