| /* |
| * 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.annotation.CurrentTimeMillisLong; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SuppressLint; |
| import android.annotation.SystemApi; |
| 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 writing to a new file and renaming it |
| * into the place of the original file after the write has successfully completed. If you need this |
| * on older versions of the platform you can use {@link androidx.core.util.AtomicFile} in AndroidX. |
| * <p> |
| * Atomic file guarantees file integrity by ensuring that a file has been completely written and |
| * sync'd to disk before renaming it to the original file. Previously this is done by renaming the |
| * original file to a backup file beforehand, but this approach couldn't handle the case where the |
| * file is created for the first time. This class will also handle the backup file created by the |
| * old implementation properly. |
| * <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. |
| */ |
| public class AtomicFile { |
| private static final String LOG_TAG = "AtomicFile"; |
| |
| private final File mBaseName; |
| private final File mNewName; |
| private final File mLegacyBackupName; |
| private SystemConfigFileCommitEventLogger mCommitEventLogger; |
| |
| /** |
| * Create a new AtomicFile for a file located at the given File path. |
| * The new file created when writing will be the same file path with ".new" appended. |
| */ |
| public AtomicFile(File baseName) { |
| this(baseName, (SystemConfigFileCommitEventLogger) null); |
| } |
| |
| /** |
| * @hide Internal constructor that also allows you to have the class |
| * automatically log commit events. |
| */ |
| public AtomicFile(File baseName, String commitTag) { |
| this(baseName, new SystemConfigFileCommitEventLogger(commitTag)); |
| } |
| |
| /** |
| * Internal constructor that also allows you to have the class |
| * automatically log commit events. |
| * |
| * @hide |
| */ |
| @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) |
| @SuppressLint("StreamFiles") |
| public AtomicFile(@NonNull File baseName, |
| @Nullable SystemConfigFileCommitEventLogger commitEventLogger) { |
| mBaseName = baseName; |
| mNewName = new File(baseName.getPath() + ".new"); |
| mLegacyBackupName = new File(baseName.getPath() + ".bak"); |
| mCommitEventLogger = commitEventLogger; |
| } |
| |
| /** |
| * 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 new files. |
| */ |
| public void delete() { |
| mBaseName.delete(); |
| mNewName.delete(); |
| mLegacyBackupName.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(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()}. |
| * |
| * @deprecated Use {@link SystemConfigFileCommitEventLogger#setStartTime} followed |
| * by {@link #startWrite()} |
| */ |
| @Deprecated |
| public FileOutputStream startWrite(long startTime) throws IOException { |
| if (mCommitEventLogger != null) { |
| if (startTime != 0) { |
| mCommitEventLogger.setStartTime(startTime); |
| } |
| |
| mCommitEventLogger.onStartWrite(); |
| } |
| |
| if (mLegacyBackupName.exists()) { |
| rename(mLegacyBackupName, mBaseName); |
| } |
| |
| try { |
| return new FileOutputStream(mNewName); |
| } catch (FileNotFoundException e) { |
| File parent = mNewName.getParentFile(); |
| if (!parent.mkdirs()) { |
| throw new IOException("Failed to create directory for " + mNewName); |
| } |
| FileUtils.setPermissions(parent.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG |
| | FileUtils.S_IXOTH, -1, -1); |
| try { |
| return new FileOutputStream(mNewName); |
| } catch (FileNotFoundException e2) { |
| throw new IOException("Failed to create new file " + mNewName, e2); |
| } |
| } |
| } |
| |
| /** |
| * 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) { |
| return; |
| } |
| if (!FileUtils.sync(str)) { |
| Log.e(LOG_TAG, "Failed to sync file output stream"); |
| } |
| try { |
| str.close(); |
| } catch (IOException e) { |
| Log.e(LOG_TAG, "Failed to close file output stream", e); |
| } |
| rename(mNewName, mBaseName); |
| if (mCommitEventLogger != null) { |
| mCommitEventLogger.onFinishWrite(); |
| } |
| } |
| |
| /** |
| * 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 delete the new file. |
| */ |
| public void failWrite(FileOutputStream str) { |
| if (str == null) { |
| return; |
| } |
| if (!FileUtils.sync(str)) { |
| Log.e(LOG_TAG, "Failed to sync file output stream"); |
| } |
| try { |
| str.close(); |
| } catch (IOException e) { |
| Log.e(LOG_TAG, "Failed to close file output stream", e); |
| } |
| if (!mNewName.delete()) { |
| Log.e(LOG_TAG, "Failed to delete new file " + mNewName); |
| } |
| } |
| |
| /** @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. You should call close() on the FileInputStream when you are |
| * done reading from it. |
| * <p> |
| * You must do your own threading protection for access to AtomicFile. |
| */ |
| public FileInputStream openRead() throws FileNotFoundException { |
| if (mLegacyBackupName.exists()) { |
| rename(mLegacyBackupName, mBaseName); |
| } |
| |
| // It was okay to call openRead() between startWrite() and finishWrite() for the first time |
| // (because there is no backup file), where openRead() would open the file being written, |
| // which makes no sense, but finishWrite() would still persist the write properly. For all |
| // subsequent writes, if openRead() was called in between, it would see a backup file and |
| // delete the file being written, the same behavior as our new implementation. So we only |
| // need a special case for the first write, and don't delete the new file in this case so |
| // that finishWrite() can still work. |
| if (mNewName.exists() && mBaseName.exists()) { |
| if (!mNewName.delete()) { |
| Log.e(LOG_TAG, "Failed to delete outdated new file " + mNewName); |
| } |
| } |
| return new FileInputStream(mBaseName); |
| } |
| |
| /** |
| * @hide |
| * Checks if the original or legacy backup file exists. |
| * @return whether the original or legacy backup file exists. |
| */ |
| public boolean exists() { |
| return mBaseName.exists() || mLegacyBackupName.exists(); |
| } |
| |
| /** |
| * Gets the last modified time of the atomic file. |
| * |
| * @return last modified time in milliseconds since epoch. Returns zero if |
| * the file does not exist or an I/O error is encountered. |
| */ |
| @CurrentTimeMillisLong |
| public long getLastModifiedTime() { |
| if (mLegacyBackupName.exists()) { |
| return mLegacyBackupName.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); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "AtomicFile[" + mBaseName + "]"; |
| } |
| |
| private static void rename(File source, File target) { |
| // We used to delete the target file before rename, but that isn't atomic, and the rename() |
| // syscall should atomically replace the target file. However in the case where the target |
| // file is a directory, a simple rename() won't work. We need to delete the file in this |
| // case because there are callers who erroneously called mBaseName.mkdirs() (instead of |
| // mBaseName.getParentFile().mkdirs()) before creating the AtomicFile, and it worked |
| // regardless, so this deletion became some kind of API. |
| if (target.isDirectory()) { |
| if (!target.delete()) { |
| Log.e(LOG_TAG, "Failed to delete file which is a directory " + target); |
| } |
| } |
| if (!source.renameTo(target)) { |
| Log.e(LOG_TAG, "Failed to rename " + source + " to " + target); |
| } |
| } |
| } |