All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.util.AtomicFile Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * 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.
 * 

* 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. *

* 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 must not directly close the given * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} * or {@link #failWrite(FileOutputStream)}. * *

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. *

* 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 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); } } 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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy