/* * Copyright (C) 2016 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 org.mozilla.thirdparty.com.google.android.exoplayer2.util; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * A helper class for performing atomic operations on a file by creating a backup file until a write * has successfully completed. * *

Atomic file guarantees file integrity by ensuring that a file has been completely written and * synced 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). * *

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. */ public final class AtomicFile { private static final String TAG = "AtomicFile"; private final File baseName; private final File backupName; /** * 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 = baseName; backupName = new File(baseName.getPath() + ".bak"); } /** Returns whether the file or its backup exists. */ public boolean exists() { return baseName.exists() || backupName.exists(); } /** Delete the atomic file. This deletes both the base and backup files. */ public void delete() { baseName.delete(); backupName.delete(); } /** * Start a new write operation on the file. This returns an {@link OutputStream} to which you can * write the new file data. If the whole data is written successfully you must call * {@link #endWrite(OutputStream)}. On failure you should call {@link OutputStream#close()} * only to free up resources used by it. * *

Example usage: * *

   *   DataOutputStream dataOutput = null;
   *   try {
   *     OutputStream outputStream = atomicFile.startWrite();
   *     dataOutput = new DataOutputStream(outputStream); // Wrapper stream
   *     dataOutput.write(data1);
   *     dataOutput.write(data2);
   *     atomicFile.endWrite(dataOutput); // Pass wrapper stream
   *   } finally{
   *     if (dataOutput != null) {
   *       dataOutput.close();
   *     }
   *   }
   * 
* *

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 OutputStream startWrite() throws IOException { // Rename the current file so it may be used as a backup during the next read if (baseName.exists()) { if (!backupName.exists()) { if (!baseName.renameTo(backupName)) { Log.w(TAG, "Couldn't rename file " + baseName + " to backup file " + backupName); } } else { baseName.delete(); } } OutputStream str; try { str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e) { File parent = baseName.getParentFile(); if (parent == null || !parent.mkdirs()) { throw new IOException("Couldn't create " + baseName, e); } // Try again now that we've created the parent directory. try { str = new AtomicFileOutputStream(baseName); } catch (FileNotFoundException e2) { throw new IOException("Couldn't create " + baseName, e2); } } 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. * * @param str Outer-most wrapper OutputStream used to write to the stream returned by {@link * #startWrite()}. * @see #startWrite() */ public void endWrite(OutputStream str) throws IOException { str.close(); // If close() throws exception, the next line is skipped. backupName.delete(); } /** * 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. * *

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 InputStream openRead() throws FileNotFoundException { restoreBackup(); return new FileInputStream(baseName); } private void restoreBackup() { if (backupName.exists()) { baseName.delete(); backupName.renameTo(baseName); } } private static final class AtomicFileOutputStream extends OutputStream { private final FileOutputStream fileOutputStream; private boolean closed = false; public AtomicFileOutputStream(File file) throws FileNotFoundException { fileOutputStream = new FileOutputStream(file); } @Override public void close() throws IOException { if (closed) { return; } closed = true; flush(); try { fileOutputStream.getFD().sync(); } catch (IOException e) { Log.w(TAG, "Failed to sync file descriptor:", e); } fileOutputStream.close(); } @Override public void flush() throws IOException { fileOutputStream.flush(); } @Override public void write(int b) throws IOException { fileOutputStream.write(b); } @Override public void write(byte[] b) throws IOException { fileOutputStream.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { fileOutputStream.write(b, off, len); } } }